From 45fa16b6b9edf0a1eaf42e2d296da57a0d04f64c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 16:55:37 +0900 Subject: [PATCH 01/68] =?UTF-8?q?[fix]:=20FeedScreen=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/navigator/navigations/FeedNavigation.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt index d6868bbd..a98b718a 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt @@ -9,6 +9,10 @@ import com.texthip.thip.ui.navigator.routes.MainTabRoutes // Feed fun NavGraphBuilder.feedNavigation(navController: NavHostController) { composable { - FeedScreen(navController) + FeedScreen( + navController = navController, + nickname = "User", + userRole = "독서가" + ) } } \ No newline at end of file From 56ab7a9dd32888d295238e35f1e9027d50023308 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 17:00:46 +0900 Subject: [PATCH 02/68] =?UTF-8?q?[feat]:=20Hilt=20=EC=A3=BC=EC=9E=85=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=B0=8F=20=EA=B8=B0=EB=B3=B8=20=ED=8B=80?= =?UTF-8?q?=20=EC=9E=A1=EA=B8=B0=20=EC=99=84=EB=A3=8C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/di/ServiceModule.kt | 12 ++- .../repository/GroupRepository.kt | 14 ++-- .../thip/data/model/service/GroupService.kt | 33 ++++++++ .../viewmodel/GroupMakeRoomViewModel.kt | 55 +++---------- .../thip/ui/group/screen/GroupScreen.kt | 8 +- .../thip/ui/group/viewmodel/GroupViewModel.kt | 11 ++- .../navigator/navigations/GroupNavigation.kt | 77 +++---------------- 7 files changed, 83 insertions(+), 127 deletions(-) rename app/src/main/java/com/texthip/thip/data/{group => model}/repository/GroupRepository.kt (98%) create mode 100644 app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt diff --git a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt index 3308a37c..1c0ec9d1 100644 --- a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt +++ b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt @@ -1,12 +1,20 @@ package com.texthip.thip.data.di +import com.texthip.thip.data.model.service.GroupService import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object ServiceModule { -// @Provides -// @Singleton + + @Provides + @Singleton + fun provideGroupService(retrofit: Retrofit): GroupService { + return retrofit.create(GroupService::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/group/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt similarity index 98% rename from app/src/main/java/com/texthip/thip/data/group/repository/GroupRepository.kt rename to app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 4fbd66fa..941af0e7 100644 --- a/app/src/main/java/com/texthip/thip/data/group/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -1,18 +1,20 @@ -package com.texthip.thip.data.group.repository +package com.texthip.thip.data.model.repository import com.texthip.thip.R +import com.texthip.thip.data.model.service.GroupService import com.texthip.thip.ui.group.myroom.mock.GroupBookData import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData import kotlinx.coroutines.delay +import javax.inject.Inject +import javax.inject.Singleton -// 그룹 데이터를 제공하는 Repository -// 실제로는 서버의 API와 통신할 거라서 다 삭제하고 함수 구조만 유지한 채 수정하면 될 듯 합니다. - -class GroupRepository { - +@Singleton +class GroupRepository @Inject constructor( + private val groupService: GroupService +) { private val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") private val roomDetailsCache = mutableMapOf() diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt new file mode 100644 index 00000000..ea4095ce --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -0,0 +1,33 @@ +package com.texthip.thip.data.model.service + +import com.texthip.thip.data.model.base.BaseResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface GroupService { + + // TODO: 실제 API 엔드포인트로 교체 필요 + @GET("groups/my") + suspend fun getMyGroups(): BaseResponse> // TODO: 실제 Response 모델로 교체 + + @GET("groups/rooms") + suspend fun getRoomSections(): BaseResponse> // TODO: 실제 Response 모델로 교체 + + @GET("user/name") + suspend fun getUserName(): BaseResponse + + @GET("groups/done") + suspend fun getDoneGroups(): BaseResponse> // TODO: 실제 Response 모델로 교체 + + @GET("groups/my-rooms") + suspend fun getMyRoomGroups(): BaseResponse> // TODO: 실제 Response 모델로 교체 + + @GET("groups/search") + suspend fun searchRooms(@Query("query") query: String): BaseResponse> // TODO: 실제 Response 모델로 교체 + + @GET("groups/room") + suspend fun getRoomDetail(@Query("roomId") roomId: Int): BaseResponse // TODO: 실제 Response 모델로 교체 + + @GET("groups/genres") + suspend fun getGenres(): BaseResponse> +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index a857e861..e20f96da 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -2,18 +2,20 @@ package com.texthip.thip.ui.group.makeroom.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.repository.GroupRepository import com.texthip.thip.ui.group.makeroom.mock.BookData -import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomRequest import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.time.LocalDate +import javax.inject.Inject -// 나중에 서버와 연동할 때 사용할 뷰모델 예시 -class GroupMakeRoomViewModel( - private val groupRepository: GroupRepository = MockGroupRepository() // 기본값으로 Mock Repository 사용 +@HiltViewModel +class GroupMakeRoomViewModel @Inject constructor( + private val groupRepository: GroupRepository ) : ViewModel() { private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) @@ -85,14 +87,12 @@ class GroupMakeRoomViewModel( try { _uiState.value = currentState.copy(isLoading = true, errorMessage = null) - val request = currentState.toRequest() - val result = groupRepository.createGroup(request) - - if (result.isSuccess) { - onSuccess() - } else { - //onError(result.message ?: "그룹 생성에 실패했습니다") - } + // TODO: 실제 API 호출로 대체 + // val request = currentState.toRequest() + // val result = groupRepository.createGroup(request) + + // 임시로 성공 처리 + onSuccess() } catch (e: Exception) { //onError("네트워크 오류가 발생했습니다: ${e.message}") } finally { @@ -105,35 +105,4 @@ class GroupMakeRoomViewModel( fun clearError() { _uiState.value = _uiState.value.copy(errorMessage = null) } -} - -// Repository 예시 -interface GroupRepository { - suspend fun createGroup(request: GroupMakeRoomRequest): ApiResult -} - -// API 응답 클래스 예시 -data class ApiResult( - val isSuccess: Boolean, - val data: T? = null, - val message: String? = null -) - -data class GroupCreateResponse( - val groupId: String, - val groupName: String -) - -// Mock Repository 구현 -class MockGroupRepository : GroupRepository { - override suspend fun createGroup(request: GroupMakeRoomRequest): ApiResult { - // 임시로 성공 응답 반환 - return ApiResult( - isSuccess = true, - data = GroupCreateResponse( - groupId = "mock_group_${System.currentTimeMillis()}", - groupName = request.roomTitle - ) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index ff2fb15d..3360610c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -13,12 +13,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.FloatingButton import com.texthip.thip.ui.common.topappbar.LogoTopAppBar @@ -39,7 +38,7 @@ fun GroupScreen( onNavigateToGroupMy: () -> Unit = {}, // 내 모임방 화면으로 이동 onNavigateToGroupRecruit: (Int) -> Unit = {}, // 모집 중인 모임방 화면으로 이동 onNavigateToGroupRoom: (Int) -> Unit = {}, // 기록장 화면으로 이동 - viewModel: GroupViewModel = viewModel() + viewModel: GroupViewModel = hiltViewModel() ) { val myGroups by viewModel.myGroups.collectAsState() val roomSections by viewModel.roomSections.collectAsState() @@ -115,7 +114,6 @@ fun GroupScreen( @Composable fun PreviewGroupScreen() { ThipTheme { - val previewViewModel = remember { GroupViewModel() } - GroupScreen(viewModel = previewViewModel) + GroupScreen() } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 561adffe..09e3250c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -2,18 +2,21 @@ package com.texthip.thip.ui.group.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.texthip.thip.data.group.repository.GroupRepository +import com.texthip.thip.data.model.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class GroupViewModel( - private val repository: GroupRepository = GroupRepository() +@HiltViewModel +class GroupViewModel @Inject constructor( + private val repository: GroupRepository ) : ViewModel() { private val _myGroups = MutableStateFlow>(emptyList()) diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 0c8adce7..014fb1dc 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -7,12 +7,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.group.makeroom.screen.GroupMakeRoomScreen import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType @@ -23,14 +22,15 @@ import com.texthip.thip.ui.group.room.screen.GroupRoomScreen import com.texthip.thip.ui.group.screen.GroupDoneScreen import com.texthip.thip.ui.group.screen.GroupScreen import com.texthip.thip.ui.group.search.screen.GroupSearchScreen +import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.navigator.extensions.navigateToAlarm import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom import com.texthip.thip.ui.navigator.extensions.navigateToGroupMy import com.texthip.thip.ui.navigator.extensions.navigateToGroupRecruit -import com.texthip.thip.ui.navigator.extensions.navigateToRecommendedGroupRecruit import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoom import com.texthip.thip.ui.navigator.extensions.navigateToGroupSearch +import com.texthip.thip.ui.navigator.extensions.navigateToRecommendedGroupRecruit import com.texthip.thip.ui.navigator.routes.GroupRoutes import com.texthip.thip.ui.navigator.routes.MainTabRoutes @@ -42,9 +42,7 @@ fun NavGraphBuilder.groupNavigation( ) { // 메인 Group 화면 composable { backStackEntry -> - val groupViewModel: GroupViewModel = viewModel( - viewModelStoreOwner = backStackEntry - ) + val groupViewModel: GroupViewModel = hiltViewModel() GroupScreen( viewModel = groupViewModel, @@ -74,7 +72,7 @@ fun NavGraphBuilder.groupNavigation( // Group MakeRoom 화면 composable { - val viewModel: GroupMakeRoomViewModel = viewModel() + val viewModel: GroupMakeRoomViewModel = hiltViewModel() GroupMakeRoomScreen( viewModel = viewModel, onNavigateBack = { @@ -88,18 +86,7 @@ fun NavGraphBuilder.groupNavigation( // Group Done 화면 composable { - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } + val groupViewModel: GroupViewModel = hiltViewModel() val userName by groupViewModel.userName.collectAsState() val doneGroups by groupViewModel.doneGroups.collectAsState() @@ -114,18 +101,7 @@ fun NavGraphBuilder.groupNavigation( // Group My 화면 composable { - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } + val groupViewModel: GroupViewModel = hiltViewModel() val myRoomGroups by groupViewModel.myRoomGroups.collectAsState() GroupMyScreen( @@ -145,18 +121,7 @@ fun NavGraphBuilder.groupNavigation( // Group Search 화면 composable { - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } + val groupViewModel: GroupViewModel = hiltViewModel() val searchGroups by groupViewModel.searchGroups.collectAsState() GroupSearchScreen( @@ -178,18 +143,7 @@ fun NavGraphBuilder.groupNavigation( composable { backStackEntry -> val route = backStackEntry.toRoute() val roomId = route.roomId - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } + val groupViewModel: GroupViewModel = hiltViewModel() // suspend 함수를 위한 LaunchedEffect 사용 var roomDetail by remember { mutableStateOf(null) } @@ -226,18 +180,7 @@ fun NavGraphBuilder.groupNavigation( composable { backStackEntry -> val route = backStackEntry.toRoute() val roomId = route.roomId - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } + val groupViewModel: GroupViewModel = hiltViewModel() // suspend 함수를 위한 LaunchedEffect 사용 var roomDetail by remember { mutableStateOf(null) } From 75fedbb0aa8a5cf542c31eb013872cf5512e8ea6 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 17:24:49 +0900 Subject: [PATCH 03/68] =?UTF-8?q?[feat]:=20=EC=9E=84=EC=8B=9C=20AuthInterc?= =?UTF-8?q?eptor=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/di/NetworkModule.kt | 7 +++-- .../thip/data/network/AuthInterceptor.kt | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt diff --git a/app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt b/app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt index 514975f8..cbe80f52 100644 --- a/app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt +++ b/app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt @@ -2,6 +2,7 @@ package com.texthip.thip.data.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.texthip.thip.BuildConfig +import com.texthip.thip.data.network.AuthInterceptor import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,16 +35,14 @@ object NetworkModule { @Singleton fun providesOkHttpClient( loggingInterceptor: HttpLoggingInterceptor, -// authInterceptor: AuthInterceptor, -// authAuthenticator: TokenAuthenticator + authInterceptor: AuthInterceptor ): OkHttpClient = OkHttpClient.Builder().apply { connectTimeout(10, TimeUnit.SECONDS) writeTimeout(10, TimeUnit.SECONDS) readTimeout(10, TimeUnit.SECONDS) + addInterceptor(authInterceptor) addInterceptor(loggingInterceptor) -// addInterceptor(authInterceptor) -// authenticator(authAuthenticator) }.build() @Provides diff --git a/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt b/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt new file mode 100644 index 00000000..13165c29 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt @@ -0,0 +1,27 @@ +package com.texthip.thip.data.network + +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AuthInterceptor @Inject constructor() : Interceptor { + + // TODO: 추후 실제 토큰 관리 시스템으로 대체 예정 + // - SharedPreferences나 DataStore에서 토큰 읽기 + // - 토큰 만료 시 자동 갱신 + // - 로그인/로그아웃 상태 관리 + private val hardcodedToken = "eyJhbGciOiJIUzI1NiJ9.eyJvYXV0aDJJZCI6Imtha2FvXzQzODE1MTU3MTEiLCJpYXQiOjE3NTQyOTQ2NTAsImV4cCI6MTc1Njg4NjY1MH0.ejCMjFf4mNEvSyliV6RQ4mPJPaGGgoKkuWX2rE0tu54" + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + // Authorization 헤더 추가 + val requestWithAuth = originalRequest.newBuilder() + .addHeader("Authorization", "Bearer $hardcodedToken") + .build() + + return chain.proceed(requestWithAuth) + } +} \ No newline at end of file From be112fc8f8b91679a5511e2d2000d47ac1b74763 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 17:46:03 +0900 Subject: [PATCH 04/68] =?UTF-8?q?[feat]:=20=EC=9E=84=EC=8B=9C=20AuthInterc?= =?UTF-8?q?eptor=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/texthip/thip/data/network/AuthInterceptor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt b/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt index 13165c29..e1a506f8 100644 --- a/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt +++ b/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt @@ -12,7 +12,7 @@ class AuthInterceptor @Inject constructor() : Interceptor { // - SharedPreferences나 DataStore에서 토큰 읽기 // - 토큰 만료 시 자동 갱신 // - 로그인/로그아웃 상태 관리 - private val hardcodedToken = "eyJhbGciOiJIUzI1NiJ9.eyJvYXV0aDJJZCI6Imtha2FvXzQzODE1MTU3MTEiLCJpYXQiOjE3NTQyOTQ2NTAsImV4cCI6MTc1Njg4NjY1MH0.ejCMjFf4mNEvSyliV6RQ4mPJPaGGgoKkuWX2rE0tu54" + private val hardcodedToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDI4MjMzNiwiZXhwIjoxNzU2ODc0MzM2fQ.NG_xDSdh8A6egIX2EAFtsqDO4lmFphTzqgzHC-r8eXY" override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() From b45b079b0b6ac8f310253b90845a98a33ad54bcf Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 17:48:41 +0900 Subject: [PATCH 05/68] =?UTF-8?q?[feat]:=20=EC=B0=B8=EC=97=AC=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=82=B4=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=97=B0=EA=B2=B0=20(#=EA=B2=B05)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/group/response/JoinedRoomsDto.kt | 24 +++++++++++++++ .../data/model/repository/GroupRepository.kt | 29 ++++++++++++------- .../thip/data/model/service/GroupService.kt | 10 ++++--- .../group/myroom/component/GroupMainCard.kt | 18 +++++++----- .../ui/group/myroom/component/GroupPager.kt | 24 ++++++++------- .../ui/group/myroom/mock/GroupCardData.kt | 8 ++--- .../thip/ui/group/screen/GroupScreen.kt | 4 +++ .../thip/ui/group/viewmodel/GroupViewModel.kt | 17 +++++------ 8 files changed, 88 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt new file mode 100644 index 00000000..a961db42 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt @@ -0,0 +1,24 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +// JoinedRoomsDto.kt +@kotlinx.serialization.Serializable +data class JoinedRoomsDto( + @SerialName("roomList") val roomList: List, + @SerialName("nickname") val nickname: String, + @SerialName("page") val page: Int, + @SerialName("size") val size: Int, + @SerialName("last") val last: Boolean, + @SerialName("first") val first: Boolean +) + +@Serializable +data class JoinedRoomDto( + @SerialName("roomId") val roomId: Int, + @SerialName("bookImageUrl") val bookImageUrl: String?, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("memberCount") val memberCount: Int, + @SerialName("userPercentage") val userPercentage: Int +) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 941af0e7..2c127400 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -1,6 +1,7 @@ package com.texthip.thip.data.model.repository import com.texthip.thip.R +import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.service.GroupService import com.texthip.thip.ui.group.myroom.mock.GroupBookData import com.texthip.thip.ui.group.myroom.mock.GroupCardData @@ -25,21 +26,29 @@ class GroupRepository @Inject constructor( Result.failure(e) } } - - suspend fun getMyGroups(): Result> { + + suspend fun getMyGroups(page: Int = 1): Result> { return try { - delay(200) - val myGroups = listOf( - GroupCardData(23, "호르몬 체인지 완독하는 방", 22, R.drawable.bookcover_sample, 40, "uibowl1"), - GroupCardData(24, "명작 읽기방", 10, R.drawable.bookcover_sample, 70, "joyce"), - GroupCardData(25, "또 다른 방", 13, R.drawable.bookcover_sample, 10, "other") - ) - Result.success(myGroups) + groupService.getJoinedRooms(page) + .handleBaseResponse() // Result + .mapCatching { data -> + data?.roomList?.map { dto -> + GroupCardData( + id = dto.roomId, + title = dto.bookTitle, + members = dto.memberCount, + imageUrl = dto.bookImageUrl, + progress = dto.userPercentage, + nickname = data.nickname + ) + }.orEmpty() + } } catch (e: Exception) { Result.failure(e) } } - + + suspend fun getRoomSections(): Result> { return try { diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index ea4095ce..5c45774b 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -1,14 +1,16 @@ package com.texthip.thip.data.model.service import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.group.response.JoinedRoomsDto import retrofit2.http.GET import retrofit2.http.Query interface GroupService { - - // TODO: 실제 API 엔드포인트로 교체 필요 - @GET("groups/my") - suspend fun getMyGroups(): BaseResponse> // TODO: 실제 Response 모델로 교체 + + @GET("rooms/home/joined") + suspend fun getJoinedRooms( + @Query("page") page: Int = 1 + ): BaseResponse @GET("groups/rooms") suspend fun getRoomSections(): BaseResponse> // TODO: 실제 Response 모델로 교체 diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt index 2feb3c94..ef35f4f9 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt @@ -25,12 +25,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.theme.ThipTheme @@ -74,11 +74,12 @@ fun GroupMainCard( verticalAlignment = Alignment.CenterVertically ) { // 책 이미지 - Image( - painter = painterResource(id = data.imageRes), + AsyncImage( + model = data.imageUrl ?: R.drawable.bookcover_sample, contentDescription = "책 이미지", modifier = Modifier - .size(width = 80.dp, height = 107.dp) + .size(width = 80.dp, height = 107.dp), + contentScale = ContentScale.Crop ) Spacer(Modifier.width(12.dp)) @@ -168,11 +169,12 @@ fun PreviewMyGroupMainCard() { ThipTheme { GroupMainCard( data = GroupCardData( + id = 1, title = "호르몬 체인지 완독하는 방", members = 22, - imageRes = R.drawable.bookcover_sample, - progress = 42, - nickname = "uibowl" + imageUrl = "https://picsum.photos/300/200?1", + progress = 40, + nickname = "uibowl1" ), onClick = {} ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt index 4ee9a057..c7fa20ac 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt @@ -125,29 +125,32 @@ fun GroupPager( } -@Preview() +@Preview @Composable fun PreviewMyGroupPager() { ThipTheme { val list = listOf( GroupCardData( + id = 1, title = "호르몬 체인지 완독하는 방", members = 22, - imageRes = R.drawable.bookcover_sample, + imageUrl = "https://picsum.photos/300/200?1", progress = 40, nickname = "uibowl1님" ), GroupCardData( + id = 2, title = "명작 읽기방", members = 10, - imageRes = R.drawable.bookcover_sample, + imageUrl = "https://picsum.photos/300/200?2", progress = 70, nickname = "joyce님" ), GroupCardData( + id = 3, title = "또 다른 방", members = 13, - imageRes = R.drawable.bookcover_sample, + imageUrl = "https://picsum.photos/300/200?3", progress = 10, nickname = "other님" ) @@ -156,27 +159,28 @@ fun PreviewMyGroupPager() { } } -@Preview() +@Preview @Composable fun PreviewSingleGroupPager() { ThipTheme { - val singleList = listOf( + val single = listOf( GroupCardData( + id = 4, title = "단일 그룹", members = 15, - imageRes = R.drawable.bookcover_sample, + imageUrl = "https://picsum.photos/300/200?4", progress = 60, nickname = "single님" ) ) - GroupPager(groupCards = singleList, onCardClick = {}) + GroupPager(groupCards = single, onCardClick = {}) } } -@Preview() +@Preview @Composable fun PreviewEmptyGroupPager() { ThipTheme { GroupPager(groupCards = emptyList(), onCardClick = {}) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt index 79b6b34b..fbbd3b3c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt @@ -3,10 +3,10 @@ package com.texthip.thip.ui.group.myroom.mock import com.texthip.thip.R data class GroupCardData( - val id: Int = 0, // 모임방 ID 추가 + val id: Int, val title: String, val members: Int, - val imageRes: Int = R.drawable.bookcover_sample, - val progress: Int, // 진행률 (0~100) + val imageUrl: String?, // 추가 + val progress: Int, // 0~100 val nickname: String -) \ No newline at end of file +) diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index 3360610c..5faca556 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -40,6 +41,9 @@ fun GroupScreen( onNavigateToGroupRoom: (Int) -> Unit = {}, // 기록장 화면으로 이동 viewModel: GroupViewModel = hiltViewModel() ) { + + LaunchedEffect(Unit) { viewModel.loadMyGroups() } + val myGroups by viewModel.myGroups.collectAsState() val roomSections by viewModel.roomSections.collectAsState() val scrollState = rememberScrollState() diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 09e3250c..a0972c57 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -20,7 +20,7 @@ class GroupViewModel @Inject constructor( ) : ViewModel() { private val _myGroups = MutableStateFlow>(emptyList()) - val myGroups: StateFlow> = _myGroups.asStateFlow() + val myGroups: StateFlow> = _myGroups private val _roomSections = MutableStateFlow>(emptyList()) val roomSections: StateFlow> = _roomSections.asStateFlow() @@ -61,16 +61,13 @@ class GroupViewModel @Inject constructor( } } } - - private fun loadMyGroups() { - viewModelScope.launch { - repository.getMyGroups() - .onSuccess { groups -> - _myGroups.value = groups - } - } + + fun loadMyGroups(page: Int = 1) = viewModelScope.launch { + repository.getMyGroups(page) + .onSuccess { _myGroups.value = it } + .onFailure { } } - + private fun loadRoomSections() { viewModelScope.launch { repository.getRoomSections() From f86acc51a2e34d0f87807a27fb971ea6a785bc6a Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 22:10:17 +0900 Subject: [PATCH 06/68] =?UTF-8?q?[feat]:=20=EB=8F=85=EC=84=9C=20=EB=AA=A8?= =?UTF-8?q?=EC=9E=84=EB=B0=A9=20DTO=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/group/response/JoinedRoomsDto.kt | 4 +- .../data/model/group/response/RoomListDto.kt | 22 +++ .../data/model/repository/GroupRepository.kt | 126 ++++++++++-------- .../thip/data/model/service/GroupService.kt | 21 +-- .../component/GroupDeadlineRoomSection.kt | 14 +- .../thip/ui/group/screen/GroupScreen.kt | 5 + .../thip/ui/group/viewmodel/GroupViewModel.kt | 39 +++++- 7 files changed, 160 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt index a961db42..acaf8265 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt @@ -3,8 +3,8 @@ package com.texthip.thip.data.model.group.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -// JoinedRoomsDto.kt -@kotlinx.serialization.Serializable + +@Serializable data class JoinedRoomsDto( @SerialName("roomList") val roomList: List, @SerialName("nickname") val nickname: String, diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt new file mode 100644 index 00000000..4efc796d --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt @@ -0,0 +1,22 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class RoomListDto( + @SerialName("roomId") val roomId: Int, + @SerialName("imageUrl") val imageUrl: String?, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("memberCount") val memberCount: Int, + @SerialName("userPercentage") val userPercentage: Float, + @SerialName("deadlineDate") val deadlineDate: String +) + +@Serializable +data class RoomsHomeDto( + @SerialName("deadlineRoomList") val deadline: List = emptyList(), + @SerialName("popularityRoomList") val popularity: List = emptyList(), + @SerialName("influencerRoomList") val influencer: List = emptyList() +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 2c127400..aab8b725 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -2,6 +2,7 @@ package com.texthip.thip.data.model.repository import com.texthip.thip.R import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.group.response.RoomListDto import com.texthip.thip.data.model.service.GroupService import com.texthip.thip.ui.group.myroom.mock.GroupBookData import com.texthip.thip.ui.group.myroom.mock.GroupCardData @@ -19,6 +20,14 @@ class GroupRepository @Inject constructor( private val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") private val roomDetailsCache = mutableMapOf() + // UI 장르명 → API 카테고리명 매핑 + private fun mapGenreToApiCategory(genre: String): String { + return when (genre) { + "과학·IT" -> "과학/IT" + else -> genre + } + } + suspend fun getUserName(): Result { return try { Result.success("규빈") // 임시 이름 @@ -49,67 +58,74 @@ class GroupRepository @Inject constructor( } - suspend fun getRoomSections(): Result> { + suspend fun getRoomSections(category: String = "문학"): Result> { return try { - - // 마감 임박한 독서 모임방 - val deadlineRooms = listOf( - GroupCardItemRoomData(1, "시집만 읽는 사람들 3월", 22, 30, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(2, "일본 소설 좋아하는 사람들", 15, 20, true, 2, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(3, "명작 같이 읽기방", 22, 30, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(4, "물리책 읽는 방", 13, 20, true, 1, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(5, "코딩 과학 동아리", 12, 15, true, 5, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(6, "사회과학 인문 탐구", 8, 12, true, 4, R.drawable.bookcover_sample, 2) - ) - - // 인기 있는 독서 모임방 - val popularRooms = listOf( - GroupCardItemRoomData(7, "베스트셀러 토론방", 28, 30, true, 7, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(8, "인기 소설 완독방", 25, 25, false, 5, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(9, "트렌드 과학서 읽기", 20, 25, true, 10, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(10, "화제의 경영서", 18, 20, true, 8, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(11, "인기 철학서 모임", 15, 18, true, 12, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(12, "예술서 베스트", 12, 15, true, 6, R.drawable.bookcover_sample, 4) - ) - - // 인플루언서, 작가 독서 모임방 - val influencerRooms = listOf( - GroupCardItemRoomData(13, "작가와 함께하는 독서방", 30, 30, false, 14, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(14, "유명 북튜버와 읽기", 18, 20, true, 8, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(15, "작가 초청 인문학방", 15, 20, true, 12, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(16, "인플루언서 과학책", 22, 25, true, 9, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(17, "유명작가 예술론", 16, 18, true, 11, R.drawable.bookcover_sample, 4) - ) - - val sections = listOf( - GroupRoomSectionData( - title = "마감 임박한 독서 모임방", - rooms = deadlineRooms, - genres = genres - ), - GroupRoomSectionData( - title = "인기 있는 독서 모임방", - rooms = popularRooms, - genres = genres - ), - GroupRoomSectionData( - title = "인플루언서·작가 독서 모임방", - rooms = influencerRooms, - genres = genres - ) - ) - - // 상세 데이터 캐시에 저장 - (deadlineRooms + popularRooms + influencerRooms).forEach { room -> - initializeRoomDetail(room) - } - - Result.success(sections) + val apiCategory = mapGenreToApiCategory(category) + groupService.getRooms(apiCategory) + .handleBaseResponse() + .mapCatching { data -> + data?.let { roomsData -> + val sections = listOf( + GroupRoomSectionData( + title = "마감 임박한 독서 모임방", + rooms = roomsData.deadline.map { dto -> + convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) + }, + genres = genres + ), + GroupRoomSectionData( + title = "인기 있는 독서 모임방", + rooms = roomsData.popularity.map { dto -> + convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) + }, + genres = genres + ), + GroupRoomSectionData( + title = "인플루언서·작가 독서 모임방", + rooms = roomsData.influencer.map { dto -> + convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) + }, + genres = genres + ) + ) + + // 상세 데이터 캐시에 저장 + val allRooms = sections.flatMap { it.rooms } + allRooms.forEach { room -> + initializeRoomDetail(room) + } + + sections + }.orEmpty() + } } catch (e: Exception) { Result.failure(e) } } + private fun convertToGroupCardItemRoomData(dto: RoomListDto, daysLeft: Int): GroupCardItemRoomData { + return GroupCardItemRoomData( + id = dto.roomId, + title = dto.bookTitle, + participants = dto.memberCount, + maxParticipants = dto.memberCount + 10, // API에서 maxParticipants를 제공하지 않으므로 임시로 +10 + isRecruiting = true, + endDate = daysLeft, + imageRes = null, + genreIndex = 0, + isSecret = false + ) + } + + private fun extractDaysFromDeadline(deadlineDate: String): Int { + return when { + deadlineDate.contains("일 뒤") -> { + deadlineDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 + } + else -> 0 // 파싱할 수 없는 경우 0 반환 + } + } + suspend fun getDoneGroups(): Result> { return try { val doneGroups = listOf( diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index 5c45774b..cad6831b 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -2,6 +2,7 @@ package com.texthip.thip.data.model.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.group.response.JoinedRoomsDto +import com.texthip.thip.data.model.group.response.RoomsHomeDto import retrofit2.http.GET import retrofit2.http.Query @@ -11,25 +12,27 @@ interface GroupService { suspend fun getJoinedRooms( @Query("page") page: Int = 1 ): BaseResponse - - @GET("groups/rooms") - suspend fun getRoomSections(): BaseResponse> // TODO: 실제 Response 모델로 교체 - + + @GET("rooms") + suspend fun getRooms( + @Query("category") category: String = "문학" // 디폴트=문학 + ): BaseResponse + @GET("user/name") suspend fun getUserName(): BaseResponse - + @GET("groups/done") suspend fun getDoneGroups(): BaseResponse> // TODO: 실제 Response 모델로 교체 - + @GET("groups/my-rooms") suspend fun getMyRoomGroups(): BaseResponse> // TODO: 실제 Response 모델로 교체 - + @GET("groups/search") suspend fun searchRooms(@Query("query") query: String): BaseResponse> // TODO: 실제 Response 모델로 교체 - + @GET("groups/room") suspend fun getRoomDetail(@Query("roomId") roomId: Int): BaseResponse // TODO: 실제 Response 모델로 교체 - + @GET("groups/genres") suspend fun getGenres(): BaseResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt index fa6984c4..fe6b76ba 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt @@ -44,6 +44,8 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun GroupRoomDeadlineSection( roomSections: List, + selectedGenreIndex: Int, + onGenreSelect: (Int) -> Unit, onRoomClick: (GroupCardItemRoomData) -> Unit ) { val sideMargin = 30.dp @@ -77,7 +79,6 @@ fun GroupRoomDeadlineSection( modifier = Modifier.fillMaxWidth() ) { page -> val section = roomSections[page] - var selectedGenre by remember { mutableIntStateOf(0) } val isCurrent = pagerState.currentPage == page val scale = if (isCurrent) 1f else 0.94f @@ -113,12 +114,13 @@ fun GroupRoomDeadlineSection( GenreChipRow( genres = section.genres, - selectedIndex = selectedGenre, - onSelect = { idx -> selectedGenre = idx } + selectedIndex = selectedGenreIndex, + onSelect = onGenreSelect ) Spacer(Modifier.height(20.dp)) - val cards = section.rooms.filter { it.genreIndex == selectedGenre } + // 서버에서 장르별로 필터링된 데이터가 오므로 모든 rooms 표시 + val cards = section.rooms Column( verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier @@ -317,6 +319,8 @@ fun PreviewGroupRoomPagerSection() { GroupRoomDeadlineSection( roomSections = roomSections, + selectedGenreIndex = 0, + onGenreSelect = {}, onRoomClick = {} ) } @@ -351,6 +355,8 @@ fun PreviewGroupRoomPagerSectionEmptyGenre() { GroupRoomDeadlineSection( roomSections = roomSections, + selectedGenreIndex = 0, + onGenreSelect = {}, onRoomClick = {} ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index 5faca556..f1cb3812 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -46,6 +46,7 @@ fun GroupScreen( val myGroups by viewModel.myGroups.collectAsState() val roomSections by viewModel.roomSections.collectAsState() + val selectedGenreIndex by viewModel.selectedGenreIndex.collectAsState() val scrollState = rememberScrollState() Box( @@ -95,6 +96,10 @@ fun GroupScreen( // 마감 임박한 독서 모임방 GroupRoomDeadlineSection( roomSections = roomSections, + selectedGenreIndex = selectedGenreIndex, + onGenreSelect = { genreIndex -> + viewModel.selectGenre(genreIndex) + }, onRoomClick = { room -> if (room.isRecruiting) { onNavigateToGroupRecruit(room.id) diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index a0972c57..1404652e 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -40,6 +40,9 @@ class GroupViewModel @Inject constructor( private val _genres = MutableStateFlow>(emptyList()) val genres: StateFlow> = _genres.asStateFlow() + private val _selectedGenreIndex = MutableStateFlow(0) + val selectedGenreIndex: StateFlow = _selectedGenreIndex.asStateFlow() + init { loadInitialData() } @@ -47,12 +50,22 @@ class GroupViewModel @Inject constructor( private fun loadInitialData() { loadUserName() loadMyGroups() + loadGenres() loadRoomSections() loadDoneGroups() loadMyRoomGroups() loadSearchGroups() } + private fun loadGenres() { + viewModelScope.launch { + repository.getGenres() + .onSuccess { genreList -> + _genres.value = genreList + } + } + } + private fun loadUserName() { viewModelScope.launch { repository.getUserName() @@ -70,7 +83,31 @@ class GroupViewModel @Inject constructor( private fun loadRoomSections() { viewModelScope.launch { - repository.getRoomSections() + val currentGenres = _genres.value + val selectedIndex = _selectedGenreIndex.value + val selectedGenre = if (currentGenres.isNotEmpty() && selectedIndex >= 0 && selectedIndex < currentGenres.size) { + currentGenres[selectedIndex] + } else { + "문학" // 기본값 + } + + repository.getRoomSections(selectedGenre) + .onSuccess { sections -> + _roomSections.value = sections + } + } + } + + fun selectGenre(genreIndex: Int) { + if (genreIndex >= 0 && genreIndex != _selectedGenreIndex.value) { + _selectedGenreIndex.value = genreIndex + loadRoomSections() // 장르 변경 시 새로운 데이터 로드 + } + } + + fun loadRoomSectionsByGenre(genre: String) { + viewModelScope.launch { + repository.getRoomSections(genre) .onSuccess { sections -> _roomSections.value = sections } From 3da5d899632190160a04fad9b178ba04c8a899d6 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 22:11:13 +0900 Subject: [PATCH 07/68] [ui]: (#65) --- .../java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt index 904abaaf..ed259267 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt @@ -1,8 +1,11 @@ package com.texthip.thip.ui.common.buttons +import android.graphics.Color import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.texthip.thip.ui.theme.ThipTheme @@ -20,6 +23,8 @@ fun GenreChipRow( ) { genres.forEachIndexed { idx, genre -> OptionChipButton( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)), // 버튼 모양에 맞게 클리핑 text = genre, isFilled = true, isSelected = selectedIndex == idx, From 67b877c0bf92e4c956ac19a5107655dd02846dfa Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 22:25:20 +0900 Subject: [PATCH 08/68] =?UTF-8?q?[feat]:=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20String=20=EC=B6=94=EC=B6=9C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/repository/GroupRepository.kt | 25 +++++++++++++------ .../thip/ui/group/viewmodel/GroupViewModel.kt | 11 +------- app/src/main/res/values/strings.xml | 5 ++++ 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index aab8b725..13b605fd 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -1,5 +1,6 @@ package com.texthip.thip.data.model.repository +import android.content.Context import com.texthip.thip.R import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.group.response.RoomListDto @@ -9,15 +10,23 @@ import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.delay import javax.inject.Inject import javax.inject.Singleton @Singleton class GroupRepository @Inject constructor( - private val groupService: GroupService + private val groupService: GroupService, + @ApplicationContext private val context: Context ) { - private val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") + private val genres = listOf( + context.getString(R.string.literature), + context.getString(R.string.science_it), + context.getString(R.string.social_science), + context.getString(R.string.humanities), + context.getString(R.string.art) + ) private val roomDetailsCache = mutableMapOf() // UI 장르명 → API 카테고리명 매핑 @@ -57,31 +66,31 @@ class GroupRepository @Inject constructor( } } - - suspend fun getRoomSections(category: String = "문학"): Result> { + suspend fun getRoomSections(category: String = ""): Result> { return try { - val apiCategory = mapGenreToApiCategory(category) + val finalCategory = if (category.isEmpty()) context.getString(R.string.literature) else category + val apiCategory = mapGenreToApiCategory(finalCategory) groupService.getRooms(apiCategory) .handleBaseResponse() .mapCatching { data -> data?.let { roomsData -> val sections = listOf( GroupRoomSectionData( - title = "마감 임박한 독서 모임방", + title = context.getString(R.string.room_section_deadline), rooms = roomsData.deadline.map { dto -> convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) }, genres = genres ), GroupRoomSectionData( - title = "인기 있는 독서 모임방", + title = context.getString(R.string.room_section_popular), rooms = roomsData.popularity.map { dto -> convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) }, genres = genres ), GroupRoomSectionData( - title = "인플루언서·작가 독서 모임방", + title = context.getString(R.string.room_section_influencer), rooms = roomsData.influencer.map { dto -> convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) }, diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 1404652e..540e7f46 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -104,16 +104,7 @@ class GroupViewModel @Inject constructor( loadRoomSections() // 장르 변경 시 새로운 데이터 로드 } } - - fun loadRoomSectionsByGenre(genre: String) { - viewModelScope.launch { - repository.getRoomSections(genre) - .onSuccess { sections -> - _roomSections.value = sections - } - } - } - + private fun loadDoneGroups() { viewModelScope.launch { repository.getDoneGroups() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9f0fc1e..a064b8fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -342,4 +342,9 @@ 직접 모임방을 만들어보세요! 모임방 만들기 + + 마감 임박한 독서 모임방 + 인기 있는 독서 모임방 + 인플루언서·작가 독서 모임방 + \ No newline at end of file From 65284f76131b527599a3c3878e4441d310b30f02 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Mon, 4 Aug 2025 22:32:47 +0900 Subject: [PATCH 09/68] =?UTF-8?q?[feat]:=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20String=20=EC=B6=94=EC=B6=9C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/model/repository/GroupRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 13b605fd..c5b41219 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -68,7 +68,7 @@ class GroupRepository @Inject constructor( suspend fun getRoomSections(category: String = ""): Result> { return try { - val finalCategory = if (category.isEmpty()) context.getString(R.string.literature) else category + val finalCategory = category.ifEmpty { context.getString(R.string.literature) } val apiCategory = mapGenreToApiCategory(finalCategory) groupService.getRooms(apiCategory) .handleBaseResponse() From 1470a64b0263c1c441a7409962f564c952bd48bd Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 10:09:10 +0900 Subject: [PATCH 10/68] =?UTF-8?q?[feat]:=20=EC=B0=B8=EC=97=AC=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=82=B4=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EC=B2=98=EB=A6=AC=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/group/response/JoinedRoomsDto.kt | 7 ++ .../data/model/repository/GroupRepository.kt | 38 ++++++--- .../ui/group/myroom/component/GroupPager.kt | 9 ++- .../thip/ui/group/viewmodel/GroupViewModel.kt | 79 ++++++++++++++++++- 4 files changed, 116 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt index acaf8265..225a5116 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt @@ -22,3 +22,10 @@ data class JoinedRoomDto( @SerialName("memberCount") val memberCount: Int, @SerialName("userPercentage") val userPercentage: Int ) + +data class PaginationResult( + val data: List, + val hasMore: Boolean, + val currentPage: Int, + val nickname: String = "" +) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index c5b41219..77734182 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -3,6 +3,7 @@ package com.texthip.thip.data.model.repository import android.content.Context import com.texthip.thip.R import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.group.response.PaginationResult import com.texthip.thip.data.model.group.response.RoomListDto import com.texthip.thip.data.model.service.GroupService import com.texthip.thip.ui.group.myroom.mock.GroupBookData @@ -44,22 +45,35 @@ class GroupRepository @Inject constructor( Result.failure(e) } } - - suspend fun getMyGroups(page: Int = 1): Result> { + suspend fun getMyRooms(page: Int): Result> { return try { groupService.getJoinedRooms(page) - .handleBaseResponse() // Result + .handleBaseResponse() .mapCatching { data -> - data?.roomList?.map { dto -> - GroupCardData( - id = dto.roomId, - title = dto.bookTitle, - members = dto.memberCount, - imageUrl = dto.bookImageUrl, - progress = dto.userPercentage, - nickname = data.nickname + data?.let { joinedRoomsDto -> + val groups = joinedRoomsDto.roomList.map { dto -> + GroupCardData( + id = dto.roomId, + title = dto.bookTitle, + members = dto.memberCount, + imageUrl = dto.bookImageUrl, + progress = dto.userPercentage, + nickname = joinedRoomsDto.nickname + ) + } + + PaginationResult( + data = groups, + hasMore = !joinedRoomsDto.last, + currentPage = joinedRoomsDto.page, + nickname = joinedRoomsDto.nickname ) - }.orEmpty() + } ?: PaginationResult( + data = emptyList(), + hasMore = false, + currentPage = page, + nickname = "" + ) } } catch (e: Exception) { Result.failure(e) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt index c7fa20ac..a6d26f9c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt @@ -25,7 +25,8 @@ import com.texthip.thip.ui.theme.ThipTheme.colors @Composable fun GroupPager( groupCards: List, - onCardClick: (GroupCardData) -> Unit + onCardClick: (GroupCardData) -> Unit, + onCardVisible: ((Int) -> Unit)? = null ) { val scale = 0.86f val desiredGap = 10.dp @@ -74,6 +75,12 @@ fun GroupPager( LaunchedEffect(groupCards.size) { pagerState.scrollToPage(startPage) } + + // 현재 보이는 카드 인덱스를 ViewModel에 알림 + LaunchedEffect(pagerState.currentPage) { + val currentPageIndex = ((pagerState.currentPage - startPage) % groupCards.size + groupCards.size) % groupCards.size + onCardVisible?.invoke(currentPageIndex) + } HorizontalPager( state = pagerState, diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 540e7f46..b0456ce8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -21,9 +21,23 @@ class GroupViewModel @Inject constructor( private val _myGroups = MutableStateFlow>(emptyList()) val myGroups: StateFlow> = _myGroups + + private val _hasMoreMyGroups = MutableStateFlow(true) + val hasMoreMyGroups: StateFlow = _hasMoreMyGroups.asStateFlow() + + private val _isLoadingMyGroups = MutableStateFlow(false) + val isLoadingMyGroups: StateFlow = _isLoadingMyGroups.asStateFlow() + + private var currentMyGroupsPage = 1 + private var loadedPagesCount = 0 + private val PAGES_PER_BATCH = 5 // 5페이지씩 미리 로드 + private val PRELOAD_THRESHOLD = 3 // 3페이지에 도달하면 다음 배치 로드 private val _roomSections = MutableStateFlow>(emptyList()) val roomSections: StateFlow> = _roomSections.asStateFlow() + + private val _isLoadingRoomSections = MutableStateFlow(false) + val isLoadingRoomSections: StateFlow = _isLoadingRoomSections.asStateFlow() private val _userName = MutableStateFlow("") val userName: StateFlow = _userName.asStateFlow() @@ -75,14 +89,66 @@ class GroupViewModel @Inject constructor( } } - fun loadMyGroups(page: Int = 1) = viewModelScope.launch { - repository.getMyGroups(page) - .onSuccess { _myGroups.value = it } - .onFailure { } + fun loadMyGroups(reset: Boolean = false) = viewModelScope.launch { + if (reset) { + currentMyGroupsPage = 1 + loadedPagesCount = 0 + _myGroups.value = emptyList() + _hasMoreMyGroups.value = true + } + // 초기 로드 시 첫 번째 배치(5페이지) 미리 로드 + loadPageBatch() + } + + private fun loadPageBatch() = viewModelScope.launch { + if (_isLoadingMyGroups.value || !_hasMoreMyGroups.value) return@launch + + _isLoadingMyGroups.value = true + + // 5페이지씩 배치로 로드 + val currentBatchStart = currentMyGroupsPage + val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 + + for (page in currentBatchStart..batchEndPage) { + if (!_hasMoreMyGroups.value) break + + repository.getMyRooms(page) + .onSuccess { paginationResult -> + _myGroups.value = _myGroups.value + paginationResult.data + _hasMoreMyGroups.value = paginationResult.hasMore + loadedPagesCount++ + currentMyGroupsPage = page + 1 + } + .onFailure { + // 에러 처리 - 배치 로딩 중단 + break + } + } + + _isLoadingMyGroups.value = false + } + + // GroupPager에서 현재 카드 인덱스를 알려주면 미리 로드 판단 + fun onCardVisible(cardIndex: Int) { + val totalCards = _myGroups.value.size + val currentPageEquivalent = (cardIndex / 3) + 1 // 3개씩 한 페이지라고 가정 + + // 현재 로드된 페이지의 3페이지 지점에 도달하면 다음 배치 로드 + if (currentPageEquivalent >= loadedPagesCount - PRELOAD_THRESHOLD && + _hasMoreMyGroups.value && + !_isLoadingMyGroups.value) { + loadPageBatch() + } + } + + fun loadMoreMyGroups() { + loadPageBatch() } private fun loadRoomSections() { viewModelScope.launch { + _isLoadingRoomSections.value = true + val currentGenres = _genres.value val selectedIndex = _selectedGenreIndex.value val selectedGenre = if (currentGenres.isNotEmpty() && selectedIndex >= 0 && selectedIndex < currentGenres.size) { @@ -95,6 +161,11 @@ class GroupViewModel @Inject constructor( .onSuccess { sections -> _roomSections.value = sections } + .onFailure { + // 에러 처리 (필요시 에러 상태 추가) + } + + _isLoadingRoomSections.value = false } } From 828801b913826e6e96f809d6cf040355e542af64 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 10:09:29 +0900 Subject: [PATCH 11/68] =?UTF-8?q?[feat]:=20GroupScreen=EC=97=90=20refreshi?= =?UTF-8?q?ng=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/group/screen/GroupScreen.kt | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index f1cb3812..b6fd19bb 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -10,8 +10,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -30,6 +31,7 @@ import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GroupScreen( onNavigateToMakeRoom: () -> Unit = {}, @@ -41,22 +43,30 @@ fun GroupScreen( onNavigateToGroupRoom: (Int) -> Unit = {}, // 기록장 화면으로 이동 viewModel: GroupViewModel = hiltViewModel() ) { - - LaunchedEffect(Unit) { viewModel.loadMyGroups() } - val myGroups by viewModel.myGroups.collectAsState() val roomSections by viewModel.roomSections.collectAsState() val selectedGenreIndex by viewModel.selectedGenreIndex.collectAsState() + val isLoadingMyGroups by viewModel.isLoadingMyGroups.collectAsState() + val isLoadingRoomSections by viewModel.isLoadingRoomSections.collectAsState() val scrollState = rememberScrollState() + + val isRefreshing = isLoadingMyGroups || isLoadingRoomSections Box( modifier = Modifier.fillMaxSize() ) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(scrollState) + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + viewModel.refreshGroupData() + }, + modifier = Modifier.fillMaxSize() ) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { // 상단바 LogoTopAppBar( leftIcon = painterResource(R.drawable.ic_done), @@ -81,6 +91,9 @@ fun GroupScreen( groupCards = myGroups, onCardClick = { groupCard -> onNavigateToGroupRoom(groupCard.id) + }, + onCardVisible = { cardIndex -> + viewModel.onCardVisible(cardIndex) } ) Spacer(Modifier.height(32.dp)) @@ -109,6 +122,7 @@ fun GroupScreen( } ) Spacer(Modifier.height(102.dp)) + } } // 오른쪽 하단 FAB FloatingButton( From 4078c52663a2a8d8032d4fb8ad1ae897d296be97 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 10:53:17 +0900 Subject: [PATCH 12/68] =?UTF-8?q?[feat]:=20GroupScreen=EC=97=90=20refreshi?= =?UTF-8?q?ng=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/group/screen/GroupScreen.kt | 5 +- .../thip/ui/group/viewmodel/GroupViewModel.kt | 107 ++++++++++++------ .../navigator/navigations/GroupNavigation.kt | 7 +- 3 files changed, 73 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index b6fd19bb..914dc952 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -46,11 +46,8 @@ fun GroupScreen( val myGroups by viewModel.myGroups.collectAsState() val roomSections by viewModel.roomSections.collectAsState() val selectedGenreIndex by viewModel.selectedGenreIndex.collectAsState() - val isLoadingMyGroups by viewModel.isLoadingMyGroups.collectAsState() - val isLoadingRoomSections by viewModel.isLoadingRoomSections.collectAsState() val scrollState = rememberScrollState() - - val isRefreshing = isLoadingMyGroups || isLoadingRoomSections + val isRefreshing by viewModel.isRefreshing.collectAsState() Box( modifier = Modifier.fillMaxSize() diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index b0456ce8..45d778e1 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -12,6 +12,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import javax.inject.Inject @HiltViewModel @@ -25,8 +27,8 @@ class GroupViewModel @Inject constructor( private val _hasMoreMyGroups = MutableStateFlow(true) val hasMoreMyGroups: StateFlow = _hasMoreMyGroups.asStateFlow() - private val _isLoadingMyGroups = MutableStateFlow(false) - val isLoadingMyGroups: StateFlow = _isLoadingMyGroups.asStateFlow() + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow = _isRefreshing.asStateFlow() private var currentMyGroupsPage = 1 private var loadedPagesCount = 0 @@ -36,8 +38,6 @@ class GroupViewModel @Inject constructor( private val _roomSections = MutableStateFlow>(emptyList()) val roomSections: StateFlow> = _roomSections.asStateFlow() - private val _isLoadingRoomSections = MutableStateFlow(false) - val isLoadingRoomSections: StateFlow = _isLoadingRoomSections.asStateFlow() private val _userName = MutableStateFlow("") val userName: StateFlow = _userName.asStateFlow() @@ -64,22 +64,11 @@ class GroupViewModel @Inject constructor( private fun loadInitialData() { loadUserName() loadMyGroups() - loadGenres() loadRoomSections() - loadDoneGroups() loadMyRoomGroups() loadSearchGroups() } - private fun loadGenres() { - viewModelScope.launch { - repository.getGenres() - .onSuccess { genreList -> - _genres.value = genreList - } - } - } - private fun loadUserName() { viewModelScope.launch { repository.getUserName() @@ -101,9 +90,7 @@ class GroupViewModel @Inject constructor( } private fun loadPageBatch() = viewModelScope.launch { - if (_isLoadingMyGroups.value || !_hasMoreMyGroups.value) return@launch - - _isLoadingMyGroups.value = true + if (!_hasMoreMyGroups.value) return@launch // 5페이지씩 배치로 로드 val currentBatchStart = currentMyGroupsPage @@ -112,7 +99,7 @@ class GroupViewModel @Inject constructor( for (page in currentBatchStart..batchEndPage) { if (!_hasMoreMyGroups.value) break - repository.getMyRooms(page) + repository.getMyJoinedRooms(page) .onSuccess { paginationResult -> _myGroups.value = _myGroups.value + paginationResult.data _hasMoreMyGroups.value = paginationResult.hasMore @@ -124,8 +111,6 @@ class GroupViewModel @Inject constructor( break } } - - _isLoadingMyGroups.value = false } // GroupPager에서 현재 카드 인덱스를 알려주면 미리 로드 판단 @@ -135,8 +120,7 @@ class GroupViewModel @Inject constructor( // 현재 로드된 페이지의 3페이지 지점에 도달하면 다음 배치 로드 if (currentPageEquivalent >= loadedPagesCount - PRELOAD_THRESHOLD && - _hasMoreMyGroups.value && - !_isLoadingMyGroups.value) { + _hasMoreMyGroups.value) { loadPageBatch() } } @@ -147,7 +131,6 @@ class GroupViewModel @Inject constructor( private fun loadRoomSections() { viewModelScope.launch { - _isLoadingRoomSections.value = true val currentGenres = _genres.value val selectedIndex = _selectedGenreIndex.value @@ -164,8 +147,6 @@ class GroupViewModel @Inject constructor( .onFailure { // 에러 처리 (필요시 에러 상태 추가) } - - _isLoadingRoomSections.value = false } } @@ -175,15 +156,6 @@ class GroupViewModel @Inject constructor( loadRoomSections() // 장르 변경 시 새로운 데이터 로드 } } - - private fun loadDoneGroups() { - viewModelScope.launch { - repository.getDoneGroups() - .onSuccess { groups -> - _doneGroups.value = groups - } - } - } private fun loadMyRoomGroups() { viewModelScope.launch { @@ -204,7 +176,70 @@ class GroupViewModel @Inject constructor( } fun refreshGroupData() { - loadInitialData() + viewModelScope.launch { + _isRefreshing.value = true + try { + // 모든 데이터를 병렬로 새로고침하고 완료를 기다림 + val jobs = listOf( + async { + repository.getUserName() + .onSuccess { userName -> + _userName.value = userName + } + }, + async { + // 내 모임방 데이터 리셋 후 로드 + currentMyGroupsPage = 1 + loadedPagesCount = 0 + _myGroups.value = emptyList() + _hasMoreMyGroups.value = true + + // 첫 번째 배치 로드 + val currentBatchStart = currentMyGroupsPage + val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 + + for (page in currentBatchStart..batchEndPage) { + if (!_hasMoreMyGroups.value) break + + repository.getMyJoinedRooms(page) + .onSuccess { paginationResult -> + _myGroups.value = _myGroups.value + paginationResult.data + _hasMoreMyGroups.value = paginationResult.hasMore + loadedPagesCount++ + currentMyGroupsPage = page + 1 + } + .onFailure { + break + } + } + }, + async { + val currentGenres = _genres.value + val selectedIndex = _selectedGenreIndex.value + val selectedGenre = if (currentGenres.isNotEmpty() && selectedIndex >= 0 && selectedIndex < currentGenres.size) { + currentGenres[selectedIndex] + } else { + "문학" // 기본값 + } + + repository.getRoomSections(selectedGenre) + .onSuccess { sections -> + _roomSections.value = sections + } + }, + async { + repository.getMyRoomGroups() + .onSuccess { groups -> + _myRoomGroups.value = groups + } + } + ) + + jobs.awaitAll() + } finally { + _isRefreshing.value = false + } + } } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 014fb1dc..9f255617 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -19,7 +19,7 @@ import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.screen.GroupMyScreen import com.texthip.thip.ui.group.room.screen.GroupRoomRecruitScreen import com.texthip.thip.ui.group.room.screen.GroupRoomScreen -import com.texthip.thip.ui.group.screen.GroupDoneScreen +import com.texthip.thip.ui.group.done.Screen.GroupDoneScreen import com.texthip.thip.ui.group.screen.GroupScreen import com.texthip.thip.ui.group.search.screen.GroupSearchScreen import com.texthip.thip.ui.group.viewmodel.GroupViewModel @@ -87,12 +87,7 @@ fun NavGraphBuilder.groupNavigation( // Group Done 화면 composable { val groupViewModel: GroupViewModel = hiltViewModel() - val userName by groupViewModel.userName.collectAsState() - val doneGroups by groupViewModel.doneGroups.collectAsState() - GroupDoneScreen( - name = userName, - allDataList = doneGroups, onNavigateBack = { navigateBack() } From ead05989dfc1ad80fe2055519c7cb871cc65bfa6 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 10:54:00 +0900 Subject: [PATCH 13/68] =?UTF-8?q?[feat]:=20=EC=99=84=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?=EB=AA=A8=EC=9E=84=EB=B0=A9=20dto=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/group/response/MyRoomsDto.kt | 20 +++++ .../thip/ui/group/done/mock/MyRoomCardData.kt | 15 ++++ .../done/viewmodel/GroupDoneViewModel.kt | 87 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomsDto.kt create mode 100644 app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt create mode 100644 app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomsDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomsDto.kt new file mode 100644 index 00000000..5e8f0de4 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomsDto.kt @@ -0,0 +1,20 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MyRoomsDto( + @SerialName("roomList") val roomList: List, + @SerialName("nextCursor") val nextCursor: String?, + @SerialName("isLast") val isLast: Boolean +) + +@Serializable +data class MyRoomDto( + @SerialName("roomId") val roomId: Int, + @SerialName("bookImageUrl") val bookImageUrl: String, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("memberCount") val memberCount: Int, + @SerialName("endDate") val endDate: String? // "완료된" 모임방의 경우 null +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt new file mode 100644 index 00000000..3c22b8be --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -0,0 +1,15 @@ +package com.texthip.thip.ui.group.done.mock + +data class MyRoomCardData( + val roomId: Int, + val bookImageUrl: String, + val bookTitle: String, + val memberCount: Int, + val endDate: String? = null +) + +data class MyRoomsPaginationResult( + val data: List, + val nextCursor: String?, + val isLast: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt new file mode 100644 index 00000000..bd29c228 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -0,0 +1,87 @@ +package com.texthip.thip.ui.group.done.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GroupDoneViewModel @Inject constructor( + private val repository: GroupRepository +) : ViewModel() { + + private val _expiredRooms = MutableStateFlow>(emptyList()) + val expiredRooms: StateFlow> = _expiredRooms.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _userName = MutableStateFlow("") + val userName: StateFlow = _userName.asStateFlow() + + private var nextCursor: String? = null + private var isLastPage = false + private var isLoadingMore = false + + init { + loadInitialData() + } + + private fun loadInitialData() { + loadUserName() + loadExpiredRooms(reset = true) + } + + private fun loadUserName() { + viewModelScope.launch { + repository.getUserName() + .onSuccess { name -> + _userName.value = name + } + } + } + + fun loadExpiredRooms(reset: Boolean = false) { + if (isLoadingMore && !reset) return + if (isLastPage && !reset) return + + viewModelScope.launch { + if (reset) { + _isLoading.value = true + nextCursor = null + isLastPage = false + _expiredRooms.value = emptyList() + } else { + isLoadingMore = true + } + + repository.getMyRoomsByType("expired", nextCursor) + .onSuccess { paginationResult -> + val currentList = if (reset) emptyList() else _expiredRooms.value + _expiredRooms.value = currentList + paginationResult.data + nextCursor = paginationResult.nextCursor + isLastPage = paginationResult.isLast + } + .onFailure { + // 에러 처리 (필요시 에러 상태 추가) + } + + _isLoading.value = false + isLoadingMore = false + } + } + + fun loadMoreExpiredRooms() { + loadExpiredRooms(reset = false) + } + + fun refreshData() { + loadExpiredRooms(reset = true) + } +} \ No newline at end of file From cc80ac7eb1a5b434988118f08292d78cf20b7c6d Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 10:54:12 +0900 Subject: [PATCH 14/68] =?UTF-8?q?[feat]:=20=EC=99=84=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?=EB=AA=A8=EC=9E=84=EB=B0=A9=20API=20=EA=B5=AC=ED=98=84=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/repository/GroupRepository.kt | 77 +++++----- .../thip/data/model/service/GroupService.kt | 14 +- .../ui/group/done/Screen/GroupDoneScreen.kt | 119 +++++++++++++++ .../thip/ui/group/screen/GroupDoneScreen.kt | 143 ------------------ 4 files changed, 168 insertions(+), 185 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt delete mode 100644 app/src/main/java/com/texthip/thip/ui/group/screen/GroupDoneScreen.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 77734182..25831a69 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -6,6 +6,8 @@ import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.group.response.PaginationResult import com.texthip.thip.data.model.group.response.RoomListDto import com.texthip.thip.data.model.service.GroupService +import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import com.texthip.thip.ui.group.done.mock.MyRoomsPaginationResult import com.texthip.thip.ui.group.myroom.mock.GroupBookData import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData @@ -29,6 +31,7 @@ class GroupRepository @Inject constructor( context.getString(R.string.art) ) private val roomDetailsCache = mutableMapOf() + private var cachedUserName: String? = null // UI 장르명 → API 카테고리명 매핑 private fun mapGenreToApiCategory(genre: String): String { @@ -38,19 +41,23 @@ class GroupRepository @Inject constructor( } } - suspend fun getUserName(): Result { + fun getUserName(): Result { return try { - Result.success("규빈") // 임시 이름 + val name = cachedUserName ?: "사용자" // 캐시된 이름이 없으면 기본값 + Result.success(name) } catch (e: Exception) { Result.failure(e) } } - suspend fun getMyRooms(page: Int): Result> { + suspend fun getMyJoinedRooms(page: Int): Result> { return try { groupService.getJoinedRooms(page) .handleBaseResponse() .mapCatching { data -> data?.let { joinedRoomsDto -> + // API 응답에서 받은 닉네임을 캐시에 저장 + cachedUserName = joinedRoomsDto.nickname + val groups = joinedRoomsDto.roomList.map { dto -> GroupCardData( id = dto.roomId, @@ -125,6 +132,39 @@ class GroupRepository @Inject constructor( Result.failure(e) } } + + // 완료된 모임방 API 연동 + suspend fun getMyRoomsByType(type: String?, cursor: String? = null): Result { + return try { + groupService.getMyRooms(type, cursor) + .handleBaseResponse() + .mapCatching { myRoomsDto -> + myRoomsDto?.let { data -> + val myRoomCards = data.roomList.map { room -> + MyRoomCardData( + roomId = room.roomId, + bookImageUrl = room.bookImageUrl, + bookTitle = room.bookTitle, + memberCount = room.memberCount, + endDate = room.endDate + ) + } + + MyRoomsPaginationResult( + data = myRoomCards, + nextCursor = data.nextCursor, + isLast = data.isLast + ) + } ?: MyRoomsPaginationResult( + data = emptyList(), + nextCursor = null, + isLast = true + ) + } + } catch (e: Exception) { + Result.failure(e) + } + } private fun convertToGroupCardItemRoomData(dto: RoomListDto, daysLeft: Int): GroupCardItemRoomData { return GroupCardItemRoomData( @@ -149,27 +189,6 @@ class GroupRepository @Inject constructor( } } - suspend fun getDoneGroups(): Result> { - return try { - val doneGroups = listOf( - GroupCardItemRoomData(18, "완료된 독서 모임방 1", 15, 20, false, null, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(19, "완료된 독서 모임방 2", 25, 30, false, null, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(20, "완료된 독서 모임방 3", 12, 15, false, null, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(21, "호르몬 체인지 완독한 방", 22, 22, false, null, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(22, "명작 읽기방 완료", 10, 10, false, null, R.drawable.bookcover_sample, 0) - ) - - // 상세 데이터 캐시에 저장 - doneGroups.forEach { room -> - initializeRoomDetail(room) - } - - Result.success(doneGroups) - } catch (e: Exception) { - Result.failure(e) - } - } - suspend fun getMyRoomGroups(): Result> { return try { val myRoomGroups = listOf( @@ -235,16 +254,6 @@ class GroupRepository @Inject constructor( Result.failure(e) } } - - suspend fun getGenres(): Result> { - return try { - delay(50) - Result.success(genres) - } catch (e: Exception) { - Result.failure(e) - } - } - private fun initializeRoomDetail(room: GroupCardItemRoomData) { val bookData = GroupBookData( diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index cad6831b..8a71acde 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -2,6 +2,7 @@ package com.texthip.thip.data.model.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.group.response.JoinedRoomsDto +import com.texthip.thip.data.model.group.response.MyRoomsDto import com.texthip.thip.data.model.group.response.RoomsHomeDto import retrofit2.http.GET import retrofit2.http.Query @@ -21,18 +22,15 @@ interface GroupService { @GET("user/name") suspend fun getUserName(): BaseResponse - @GET("groups/done") - suspend fun getDoneGroups(): BaseResponse> // TODO: 실제 Response 모델로 교체 - - @GET("groups/my-rooms") - suspend fun getMyRoomGroups(): BaseResponse> // TODO: 실제 Response 모델로 교체 - @GET("groups/search") suspend fun searchRooms(@Query("query") query: String): BaseResponse> // TODO: 실제 Response 모델로 교체 @GET("groups/room") suspend fun getRoomDetail(@Query("roomId") roomId: Int): BaseResponse // TODO: 실제 Response 모델로 교체 - @GET("groups/genres") - suspend fun getGenres(): BaseResponse> + @GET("rooms/my") + suspend fun getMyRooms( + @Query("type") type: String? = null, // "playing", "recruiting", "expired", null + @Query("cursor") cursor: String? = null + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt new file mode 100644 index 00000000..25a4cc6d --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt @@ -0,0 +1,119 @@ +package com.texthip.thip.ui.group.done.Screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +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.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 com.texthip.thip.R +import com.texthip.thip.ui.common.cards.CardItemRoom +import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.group.done.viewmodel.GroupDoneViewModel +import com.texthip.thip.ui.theme.ThipTheme +import com.texthip.thip.ui.theme.ThipTheme.colors +import com.texthip.thip.ui.theme.ThipTheme.typography + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupDoneScreen( + onNavigateBack: () -> Unit = {}, + viewModel: GroupDoneViewModel = hiltViewModel() +) { + val expiredRooms by viewModel.expiredRooms.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + val userName by viewModel.userName.collectAsState() + val listState = rememberLazyListState() + + // 무한 스크롤을 위한 로직 + val shouldLoadMore by remember { + derivedStateOf { + val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + val totalItems = listState.layoutInfo.totalItemsCount + lastVisibleIndex >= totalItems - 3 // 마지막 3개 아이템에 도달했을 때 + } + } + + LaunchedEffect(shouldLoadMore) { + if (shouldLoadMore && expiredRooms.isNotEmpty()) { + viewModel.loadMoreExpiredRooms() + } + } + + Column( + Modifier.fillMaxSize() + ) { + DefaultTopAppBar( + title = stringResource(R.string.group_done_title), + onLeftClick = onNavigateBack, + ) + + PullToRefreshBox( + isRefreshing = isLoading, + onRefresh = { viewModel.refreshData() }, + modifier = Modifier.fillMaxSize() + ) { + Column( + Modifier + .background(colors.Black) + .fillMaxSize() + .padding(horizontal = 20.dp) + ) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues(bottom = 20.dp), + modifier = Modifier + .fillMaxSize() + .padding(top = 16.dp) + ) { + item { + Text( + text = stringResource(R.string.group_done_user_comment, userName), + color = colors.White, + style = typography.menu_r400_s14_h24 + ) + } + + items(expiredRooms) { room -> + CardItemRoom( + title = room.bookTitle, + participants = room.memberCount, + maxParticipants = room.memberCount, // 완료된 모임방은 maxParticipants = memberCount + isRecruiting = false, // 완료된 모임방은 항상 false + onClick = { /* 완료된 모임방은 클릭 불가 */ } + ) + } + } + } + } + } +} + + + +@Preview +@Composable +fun GroupDoneScreenPreview() { + ThipTheme { + // Preview에서는 ViewModel을 사용할 수 없으므로 기본 레이아웃만 표시 + GroupDoneScreen() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupDoneScreen.kt deleted file mode 100644 index 205b0423..00000000 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupDoneScreen.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.texthip.thip.ui.group.screen - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -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 com.texthip.thip.R -import com.texthip.thip.ui.common.cards.CardItemRoom -import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -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 GroupDoneScreen( - name: String, - allDataList: List, - onNavigateBack: () -> Unit = {} -) { - val doneList = remember(allDataList) { - allDataList.filter { !it.isRecruiting } - } - - Column( - Modifier - .fillMaxSize() - ) { - DefaultTopAppBar( - title = stringResource(R.string.group_done_title), - onLeftClick = onNavigateBack, - ) - Column( - Modifier - .background(colors.Black) - .fillMaxSize() - .padding(horizontal = 20.dp) - ) { - - LazyColumn( - verticalArrangement = Arrangement.spacedBy(20.dp), - contentPadding = PaddingValues(bottom = 20.dp), - modifier = Modifier - .fillMaxSize() - .padding(top = 16.dp) - ) { - - item { - Text ( - text = stringResource(R.string.group_done_user_comment, name), - color = colors.White, - style = typography.menu_r400_s14_h24 - ) - } - - items(doneList) { item -> - CardItemRoom( - title = item.title, - participants = item.participants, - maxParticipants = item.maxParticipants, - isRecruiting = item.isRecruiting, - imageRes = item.imageRes, - onClick = { /* 완료된 모임방은 클릭 불가 */ } - ) - } - } - } - } -} - - - -@Preview -@Composable -fun MyGroupListFilterScreenPreview() { - ThipTheme { - val dataList = listOf( - GroupCardItemRoomData( - id = 1, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 2, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 3, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 4, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 5, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 6, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ) - ) - - GroupDoneScreen( - name = "rbqks529", - allDataList = dataList) - } -} \ No newline at end of file From c8b900dba3e50913be0758480ea39ccafaab5b09 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 16:23:50 +0900 Subject: [PATCH 15/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=A7=91=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20dto=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/group/response/RoomRecruitingDto.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingDto.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingDto.kt new file mode 100644 index 00000000..5cd95c53 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingDto.kt @@ -0,0 +1,36 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomRecruitingDto( + @SerialName("isHost") val isHost: Boolean, + @SerialName("isJoining") val isJoining: Boolean, + @SerialName("roomId") val roomId: Int, + @SerialName("roomName") val roomName: String, + @SerialName("roomImageUrl") val roomImageUrl: String?, + @SerialName("isPublic") val isPublic: Boolean, + @SerialName("progressStartDate") val progressStartDate: String, + @SerialName("progressEndDate") val progressEndDate: String, + @SerialName("recruitEndDate") val recruitEndDate: String, + @SerialName("category") val category: String, + @SerialName("roomDescription") val roomDescription: String, + @SerialName("memberCount") val memberCount: Int, + @SerialName("recruitCount") val recruitCount: Int, + @SerialName("isbn") val isbn: String, + @SerialName("bookImageUrl") val bookImageUrl: String, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("authorName") val authorName: String, + @SerialName("bookDescription") val bookDescription: String, + @SerialName("recommandRooms") val recommendRooms: List +) + +@Serializable +data class RecommendRoomDto( + @SerialName("roomImageUrl") val roomImageUrl: String?, + @SerialName("roomName") val roomName: String, + @SerialName("memberCount") val memberCount: Int, + @SerialName("recruitCount") val recruitCount: Int, + @SerialName("recruitEndDate") val recruitEndDate: String +) \ No newline at end of file From 927a90f061f84c960899bc788c388790ad2fb458 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 16:25:28 +0900 Subject: [PATCH 16/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=A7=91=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20Service,=20Repository?= =?UTF-8?q?=EA=B5=AC=ED=98=84(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/repository/GroupRepository.kt | 65 +++++++++++++++++++ .../thip/data/model/service/GroupService.kt | 15 ++--- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 25831a69..6645edc5 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -9,6 +9,7 @@ import com.texthip.thip.data.model.service.GroupService import com.texthip.thip.ui.group.done.mock.MyRoomCardData import com.texthip.thip.ui.group.done.mock.MyRoomsPaginationResult import com.texthip.thip.ui.group.myroom.mock.GroupBookData +import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomData @@ -239,6 +240,70 @@ class GroupRepository @Inject constructor( } } + // 모집중인 모임방 상세 정보 API 연동 + suspend fun getRoomRecruiting(roomId: Int): Result { + return try { + groupService.getRoomRecruiting(roomId) + .handleBaseResponse() + .mapCatching { recruitingDto -> + recruitingDto?.let { data -> + // 책 정보 변환 + val bookData = GroupBookData( + title = data.bookTitle, + author = data.authorName, + publisher = "출판사 정보 없음", // API에서 제공하지 않음 + description = data.bookDescription + ) + + // 추천 모임방 변환 + val recommendations = data.recommendRooms.map { recommendDto -> + GroupCardItemRoomData( + id = recommendDto.hashCode(), // 임시 ID (실제로는 roomId가 필요) + title = recommendDto.roomName, + participants = recommendDto.memberCount, + maxParticipants = recommendDto.recruitCount, + isRecruiting = true, + endDate = extractDaysFromDeadline(recommendDto.recruitEndDate), + imageRes = null, // 이미지는 URL로 처리 + genreIndex = 0, // 기본값 + isSecret = true // 기본값 + ) + } + + // GroupRoomData로 변환 + GroupRoomData( + id = data.roomId, + title = data.roomName, + isSecret = !data.isPublic, + description = data.roomDescription, + startDate = data.progressStartDate, + endDate = data.progressEndDate, + members = data.memberCount, + maxMembers = data.recruitCount, + daysLeft = extractDaysFromDeadline(data.recruitEndDate), + genre = data.category, + bookData = bookData, + recommendations = recommendations, + buttonType = determineButtonType(data.isHost, data.isJoining), + roomImageUrl = data.roomImageUrl, + bookImageUrl = data.bookImageUrl + ) + } ?: throw Exception("No recruiting data found for room $roomId") + } + } catch (e: Exception) { + Result.failure(e) + } + } + + // 버튼 타입 결정 로직 + private fun determineButtonType(isHost: Boolean, isJoining: Boolean): GroupBottomButtonType { + return when { + isHost -> GroupBottomButtonType.CLOSE // 호스트는 모집 마감 가능 + isJoining -> GroupBottomButtonType.CANCEL // 참여 중이면 취소 가능 + else -> GroupBottomButtonType.JOIN // 참여하지 않았으면 참여 가능 + } + } + suspend fun searchRooms(query: String): Result> { return try { val searchResult = getSearchGroups() diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index 8a71acde..3fcc4485 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -3,8 +3,10 @@ package com.texthip.thip.data.model.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.group.response.JoinedRoomsDto import com.texthip.thip.data.model.group.response.MyRoomsDto +import com.texthip.thip.data.model.group.response.RoomRecruitingDto import com.texthip.thip.data.model.group.response.RoomsHomeDto import retrofit2.http.GET +import retrofit2.http.Path import retrofit2.http.Query interface GroupService { @@ -19,18 +21,13 @@ interface GroupService { @Query("category") category: String = "문학" // 디폴트=문학 ): BaseResponse - @GET("user/name") - suspend fun getUserName(): BaseResponse - - @GET("groups/search") - suspend fun searchRooms(@Query("query") query: String): BaseResponse> // TODO: 실제 Response 모델로 교체 - - @GET("groups/room") - suspend fun getRoomDetail(@Query("roomId") roomId: Int): BaseResponse // TODO: 실제 Response 모델로 교체 - @GET("rooms/my") suspend fun getMyRooms( @Query("type") type: String? = null, // "playing", "recruiting", "expired", null @Query("cursor") cursor: String? = null ): BaseResponse + + @GET("rooms/{roomId}/recruiting") + suspend fun getRoomRecruiting(@Path("roomId") roomId: Int): BaseResponse + } \ No newline at end of file From a0167a93b4235a1c9fa958d1a5d323f8647dcb96 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 16:40:25 +0900 Subject: [PATCH 17/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=A7=91=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20API=EC=99=80=20Screen?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/GroupDeadlineRoomSection.kt | 159 +++++++++++------ .../ui/group/myroom/mock/GroupRoomData.kt | 5 +- .../room/screen/GroupRoomRecruitScreen.kt | 166 +++++++++++------- .../thip/ui/group/screen/GroupScreen.kt | 2 + .../thip/ui/group/viewmodel/GroupViewModel.kt | 17 +- 5 files changed, 228 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt index fe6b76ba..6ad0df14 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -19,10 +18,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -45,16 +40,12 @@ import com.texthip.thip.ui.theme.ThipTheme.typography fun GroupRoomDeadlineSection( roomSections: List, selectedGenreIndex: Int, + errorMessage: String? = null, onGenreSelect: (Int) -> Unit, onRoomClick: (GroupCardItemRoomData) -> Unit ) { val sideMargin = 30.dp - val pagerState = rememberPagerState( - initialPage = 0, - pageCount = { roomSections.size } - ) - Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -68,19 +59,42 @@ fun GroupRoomDeadlineSection( val horizontalPadding = sideMargin val cardWidth = maxWidth - (horizontalPadding * 2) val scale = 0.94f - val desiredGap = 12.dp // TODO: 이 부분을 10dp로 하면 양 옆의 카드에 살짝 다음 내용이 보여서 12정도가 어떤지 + val desiredGap = 12.dp val pageSpacing = (-(cardWidth - (cardWidth * scale)) / 2) + desiredGap + // 데이터가 없어도 기본 구조 표시 + val effectiveRoomSections = roomSections.ifEmpty { + // 기본 구조를 위한 더미 섹션 생성 + listOf( + GroupRoomSectionData( + title = stringResource(R.string.room_section_deadline), + rooms = emptyList(), + genres = listOf( + stringResource(R.string.literature), + stringResource(R.string.science_it), + stringResource(R.string.social_science), + stringResource(R.string.humanities), + stringResource(R.string.art) + ) + ) + ) + } + + val effectivePagerState = rememberPagerState( + initialPage = 0, + pageCount = { effectiveRoomSections.size } + ) + HorizontalPager( - state = pagerState, + state = effectivePagerState, contentPadding = PaddingValues(horizontal = 30.dp), pageSpacing = pageSpacing, modifier = Modifier.fillMaxWidth() ) { page -> - val section = roomSections[page] + val section = effectiveRoomSections[page] - val isCurrent = pagerState.currentPage == page + val isCurrent = effectivePagerState.currentPage == page val scale = if (isCurrent) 1f else 0.94f Box( @@ -119,60 +133,87 @@ fun GroupRoomDeadlineSection( ) Spacer(Modifier.height(20.dp)) - // 서버에서 장르별로 필터링된 데이터가 오므로 모든 rooms 표시 - val cards = section.rooms Column( verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier .fillMaxWidth() .height(584.dp) ) { - if (cards.isEmpty()) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(Modifier.height(40.dp)) - Text( - text = stringResource(R.string.group_no_room_exist), - style = typography.smalltitle_sb600_s16_h20, - color = colors.White, - textAlign = TextAlign.Center - ) - Spacer(Modifier.height(8.dp)) - Text( - text = stringResource(R.string.group_no_room_error_comment), - style = typography.copy_r400_s14, - color = colors.Grey, - textAlign = TextAlign.Center - ) + when { + // 에러 상태 + errorMessage != null -> { + Column( + modifier = Modifier + .padding(top = 30.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.error_data_load_failed), + style = typography.smalltitle_sb600_s16_h20, + color = colors.White, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(8.dp)) + Text( + text = errorMessage, + style = typography.copy_r400_s14, + color = colors.Grey, + textAlign = TextAlign.Center + ) + } } - } else { - Column( - verticalArrangement = Arrangement.spacedBy(20.dp), - modifier = Modifier.fillMaxWidth() - ) { - cards.forEach { room -> - CardItemRoom( - title = room.title, - participants = room.participants, - maxParticipants = room.maxParticipants, - isRecruiting = room.isRecruiting, - endDate = room.endDate, - imageRes = room.imageRes, - onClick = { onRoomClick(room) }, - hasBorder = true, + // 데이터 없음 상태 + section.rooms.isEmpty() -> { + Column( + modifier = Modifier + .padding(top = 30.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.group_no_room_exist), + style = typography.smalltitle_sb600_s16_h20, + color = colors.White, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.group_no_room_error_comment), + style = typography.copy_r400_s14, + color = colors.Grey, + textAlign = TextAlign.Center + ) + } + } + // 정상 데이터 표시 + else -> { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier.fillMaxWidth() + ) { + section.rooms.forEach { room -> + CardItemRoom( + title = room.title, + participants = room.participants, + maxParticipants = room.maxParticipants, + isRecruiting = room.isRecruiting, + endDate = room.endDate, + imageRes = room.imageRes, + onClick = { onRoomClick(room) }, + hasBorder = true, + ) + } + } + + if (section.rooms.size < 4) { + Spacer( + modifier = Modifier + .weight(1f, fill = true) + .fillMaxWidth() ) } } - } - - if (cards.size < 4) { - Spacer( - modifier = Modifier - .weight(1f, fill = true) - .fillMaxWidth() - ) } } } @@ -320,6 +361,7 @@ fun PreviewGroupRoomPagerSection() { GroupRoomDeadlineSection( roomSections = roomSections, selectedGenreIndex = 0, + errorMessage = null, onGenreSelect = {}, onRoomClick = {} ) @@ -356,6 +398,7 @@ fun PreviewGroupRoomPagerSectionEmptyGenre() { GroupRoomDeadlineSection( roomSections = roomSections, selectedGenreIndex = 0, + errorMessage = null, onGenreSelect = {}, onRoomClick = {} ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupRoomData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupRoomData.kt index 4a227a4c..42bec61f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupRoomData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupRoomData.kt @@ -12,5 +12,8 @@ data class GroupRoomData( val daysLeft: Int, val genre: String, val bookData: GroupBookData, - val recommendations: List + val recommendations: List, + val buttonType: GroupBottomButtonType? = null, // API에서 결정된 버튼 타입 + val roomImageUrl: String? = null, // 방 대표 이미지 URL + val bookImageUrl: String? = null // 책 이미지 URL ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index 4ff90801..b73c8d4a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -35,6 +36,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import com.texthip.thip.R import com.texthip.thip.ui.common.cards.CardItemRoomSmall import com.texthip.thip.ui.common.cards.CardRoomBook @@ -45,6 +48,7 @@ import com.texthip.thip.ui.group.myroom.mock.GroupBookData import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -52,8 +56,9 @@ import kotlinx.coroutines.delay @Composable fun GroupRoomRecruitScreen( - detail: GroupRoomData, - buttonType: GroupBottomButtonType, + roomId: Int, + viewModel: GroupViewModel? = hiltViewModel(), + mockRoomDetail: GroupRoomData? = null, // Preview용 mock 데이터 onRecommendationClick: (GroupCardItemRoomData) -> Unit = {}, onParticipation: () -> Unit = {}, // 참여 onCancelParticipation: () -> Unit = {}, // 참여 취소 @@ -61,15 +66,49 @@ fun GroupRoomRecruitScreen( onBackClick: () -> Unit = {} // 뒤로가기 추가 ) { val context = LocalContext.current - var currentButtonType by remember { mutableStateOf(buttonType) } + + var roomDetail by remember { mutableStateOf(mockRoomDetail) } + var isLoading by remember { mutableStateOf(mockRoomDetail == null && viewModel != null) } + + var currentButtonType by remember { mutableStateOf(mockRoomDetail?.buttonType) } var showToast by remember { mutableStateOf(false) } var toastMessage by remember { mutableStateOf("") } var showDialog by remember { mutableStateOf(false) } var dialogTitle by remember { mutableStateOf("") } var dialogDescription by remember { mutableStateOf("") } var pendingAction by remember { mutableStateOf<(() -> Unit)?>(null) } + + // 데이터 로딩 - ViewModel이 있고 mockRoomDetail이 없을 때만 실행 + LaunchedEffect(roomId, viewModel) { + if (viewModel != null && mockRoomDetail == null) { + isLoading = true + + viewModel.getRoomRecruiting(roomId) + .onSuccess { data -> + roomDetail = data + currentButtonType = data.buttonType + isLoading = false + } + .onFailure { error -> + isLoading = false + } + } + } Box(Modifier.fillMaxSize()) { + // 로딩 상태 + if (isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = colors.White) + } + return@Box + } + + // 데이터가 없는 경우 + val detail = roomDetail ?: return@Box Image( painter = painterResource(id = R.drawable.group_room_recruiting), contentDescription = "배경 이미지", @@ -327,63 +366,67 @@ fun GroupRoomRecruitScreen( } // 하단 버튼 - val buttonText = when (currentButtonType) { - GroupBottomButtonType.JOIN -> stringResource(R.string.group_room_screen_participant) - GroupBottomButtonType.CANCEL -> stringResource(R.string.group_room_screen_cancel) - GroupBottomButtonType.CLOSE -> stringResource(R.string.group_room_screen_end) - } - - Button( - onClick = { - when (currentButtonType) { - GroupBottomButtonType.JOIN -> { - onParticipation() // 외부 콜백 호출 - showToast = true - toastMessage = context.getString(R.string.group_participant_complete_alarm) - currentButtonType = GroupBottomButtonType.CANCEL - } + if (currentButtonType != null) { + val buttonText = when (currentButtonType) { + GroupBottomButtonType.JOIN -> stringResource(R.string.group_room_screen_participant) + GroupBottomButtonType.CANCEL -> stringResource(R.string.group_room_screen_cancel) + GroupBottomButtonType.CLOSE -> stringResource(R.string.group_room_screen_end) + null -> TODO() + } - GroupBottomButtonType.CANCEL -> { - dialogTitle = context.getString(R.string.group_participant_cancel_popup) - dialogDescription = - context.getString(R.string.group_participant_cancel_comment) - pendingAction = { - onCancelParticipation() + Button( + onClick = { + when (currentButtonType) { + GroupBottomButtonType.JOIN -> { + onParticipation() // 외부 콜백 호출 showToast = true - toastMessage = - context.getString(R.string.group_participant_cancel_alarm) - currentButtonType = GroupBottomButtonType.JOIN + toastMessage = context.getString(R.string.group_participant_complete_alarm) + currentButtonType = GroupBottomButtonType.CANCEL } - showDialog = true - } - GroupBottomButtonType.CLOSE -> { - dialogTitle = context.getString(R.string.group_participant_close_popup) - dialogDescription = - context.getString(R.string.group_participant_close_comment) - pendingAction = { - onCloseRecruitment() - showToast = true - toastMessage = context.getString(R.string.group_participant_close_alarm) + GroupBottomButtonType.CANCEL -> { + dialogTitle = context.getString(R.string.group_participant_cancel_popup) + dialogDescription = + context.getString(R.string.group_participant_cancel_comment) + pendingAction = { + onCancelParticipation() + showToast = true + toastMessage = + context.getString(R.string.group_participant_cancel_alarm) + currentButtonType = GroupBottomButtonType.JOIN + } + showDialog = true } - showDialog = true + + GroupBottomButtonType.CLOSE -> { + dialogTitle = context.getString(R.string.group_participant_close_popup) + dialogDescription = + context.getString(R.string.group_participant_close_comment) + pendingAction = { + onCloseRecruitment() + showToast = true + toastMessage = context.getString(R.string.group_participant_close_alarm) + } + showDialog = true + } + null -> {} // currentButtonType이 null인 경우 아무것도 하지 않음 } - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = colors.Purple - ), - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(50.dp), - shape = RoundedCornerShape(0.dp) - ) { - Text( - text = buttonText, - style = typography.smalltitle_sb600_s18_h24, - color = colors.White - ) + }, + colors = ButtonDefaults.buttonColors( + containerColor = colors.Purple + ), + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(50.dp), + shape = RoundedCornerShape(0.dp) + ) { + Text( + text = buttonText, + style = typography.smalltitle_sb600_s18_h24, + color = colors.White + ) + } } // 토스트 팝업 @@ -506,8 +549,9 @@ fun GroupRoomRecruitScreenPreviewJoin() { ) GroupRoomRecruitScreen( - detail = detailJoin, - buttonType = GroupBottomButtonType.JOIN, + roomId = 1, + viewModel = null, + mockRoomDetail = detailJoin.copy(buttonType = GroupBottomButtonType.JOIN), onRecommendationClick = {}, onParticipation = {}, onCancelParticipation = {}, @@ -575,8 +619,9 @@ fun GroupRoomRecruitScreenPreviewCancel() { ) GroupRoomRecruitScreen( - detail = detailCancel, - buttonType = GroupBottomButtonType.CANCEL, + roomId = 2, + viewModel = null, + mockRoomDetail = detailCancel.copy(buttonType = GroupBottomButtonType.CANCEL), onRecommendationClick = {}, onParticipation = {}, onCancelParticipation = {}, @@ -644,8 +689,9 @@ fun GroupRoomRecruitScreenClose() { ) GroupRoomRecruitScreen( - detail = detailClose, - buttonType = GroupBottomButtonType.CLOSE, + roomId = 3, + viewModel = null, + mockRoomDetail = detailClose.copy(buttonType = GroupBottomButtonType.CLOSE), onRecommendationClick = {}, onParticipation = {}, onCancelParticipation = {}, diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index 914dc952..73d89dc6 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -48,6 +48,7 @@ fun GroupScreen( val selectedGenreIndex by viewModel.selectedGenreIndex.collectAsState() val scrollState = rememberScrollState() val isRefreshing by viewModel.isRefreshing.collectAsState() + val roomSectionsError by viewModel.roomSectionsError.collectAsState() Box( modifier = Modifier.fillMaxSize() @@ -107,6 +108,7 @@ fun GroupScreen( GroupRoomDeadlineSection( roomSections = roomSections, selectedGenreIndex = selectedGenreIndex, + errorMessage = roomSectionsError, onGenreSelect = { genreIndex -> viewModel.selectGenre(genreIndex) }, diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 45d778e1..c333c54e 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -38,6 +38,9 @@ class GroupViewModel @Inject constructor( private val _roomSections = MutableStateFlow>(emptyList()) val roomSections: StateFlow> = _roomSections.asStateFlow() + private val _roomSectionsError = MutableStateFlow(null) + val roomSectionsError: StateFlow = _roomSectionsError.asStateFlow() + private val _userName = MutableStateFlow("") val userName: StateFlow = _userName.asStateFlow() @@ -131,6 +134,7 @@ class GroupViewModel @Inject constructor( private fun loadRoomSections() { viewModelScope.launch { + _roomSectionsError.value = null val currentGenres = _genres.value val selectedIndex = _selectedGenreIndex.value @@ -144,8 +148,8 @@ class GroupViewModel @Inject constructor( .onSuccess { sections -> _roomSections.value = sections } - .onFailure { - // 에러 처리 (필요시 에러 상태 추가) + .onFailure { error -> + _roomSectionsError.value = error.message } } } @@ -214,6 +218,8 @@ class GroupViewModel @Inject constructor( } }, async { + _roomSectionsError.value = null + val currentGenres = _genres.value val selectedIndex = _selectedGenreIndex.value val selectedGenre = if (currentGenres.isNotEmpty() && selectedIndex >= 0 && selectedIndex < currentGenres.size) { @@ -226,6 +232,9 @@ class GroupViewModel @Inject constructor( .onSuccess { sections -> _roomSections.value = sections } + .onFailure { error -> + _roomSectionsError.value = error.message + } }, async { repository.getMyRoomGroups() @@ -246,5 +255,9 @@ class GroupViewModel @Inject constructor( suspend fun getRoomDetail(roomId: Int): GroupRoomData? { return repository.getRoomDetail(roomId).getOrNull() } + + suspend fun getRoomRecruiting(roomId: Int): Result { + return repository.getRoomRecruiting(roomId) + } } \ No newline at end of file From 9307e86dab0563203c44f4bda2c2c8ffd049c6e0 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 16:44:26 +0900 Subject: [PATCH 18/68] =?UTF-8?q?[feat]:=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20String=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigator/navigations/GroupNavigation.kt | 51 +++++++------------ app/src/main/res/values/strings.xml | 4 ++ 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 9f255617..e6adb926 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -12,14 +12,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.toRoute +import com.texthip.thip.ui.group.done.Screen.GroupDoneScreen import com.texthip.thip.ui.group.makeroom.screen.GroupMakeRoomScreen import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel -import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.screen.GroupMyScreen import com.texthip.thip.ui.group.room.screen.GroupRoomRecruitScreen import com.texthip.thip.ui.group.room.screen.GroupRoomScreen -import com.texthip.thip.ui.group.done.Screen.GroupDoneScreen import com.texthip.thip.ui.group.screen.GroupScreen import com.texthip.thip.ui.group.search.screen.GroupSearchScreen import com.texthip.thip.ui.group.viewmodel.GroupViewModel @@ -138,37 +137,25 @@ fun NavGraphBuilder.groupNavigation( composable { backStackEntry -> val route = backStackEntry.toRoute() val roomId = route.roomId - val groupViewModel: GroupViewModel = hiltViewModel() - - // suspend 함수를 위한 LaunchedEffect 사용 - var roomDetail by remember { mutableStateOf(null) } - LaunchedEffect(roomId) { - roomDetail = groupViewModel.getRoomDetail(roomId) - } - roomDetail?.let { detail -> - GroupRoomRecruitScreen( - detail = detail, - buttonType = GroupBottomButtonType.JOIN, // 기본값, 실제로는 사용자 상태에 따라 결정 - onRecommendationClick = { recommendation -> - navController.navigateToRecommendedGroupRecruit(recommendation.id) - }, - onParticipation = { - // 참여 로직 - }, - onCancelParticipation = { - // 참여 취소 로직 - }, - onCloseRecruitment = { - // 모집 마감 로직 - }, - onBackClick = { - navigateBack() - } - ) - } ?: run { - // 로딩 중이거나 데이터를 찾을 수 없는 경우 - } + GroupRoomRecruitScreen( + roomId = roomId, + onRecommendationClick = { recommendation -> + navController.navigateToRecommendedGroupRecruit(recommendation.id) + }, + onParticipation = { + // TODO: 참여 API 호출 + }, + onCancelParticipation = { + // TODO: 참여 취소 API 호출 + }, + onCloseRecruitment = { + // TODO: 모집 마감 API 호출 + }, + onBackClick = { + navigateBack() + } + ) } // Group Room 화면 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a064b8fc..13623010 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,5 +346,9 @@ 마감 임박한 독서 모임방 인기 있는 독서 모임방 인플루언서·작가 독서 모임방 + + + 데이터를 불러올 수 없습니다 + 네트워크 연결을 확인해주세요 \ No newline at end of file From 6ef1fb00d43cd3d54f9bd43834b47d0b3fc09e88 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 16:55:24 +0900 Subject: [PATCH 19/68] =?UTF-8?q?[feat]:=20GroupScreen=20=EB=8F=85?= =?UTF-8?q?=EC=84=9C=EB=AA=A8=EC=9E=84=EB=B0=A9=20API=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/group/response/RoomListDto.kt | 11 +++---- .../data/model/repository/GroupRepository.kt | 32 +++---------------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt index 4efc796d..db3cd48e 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt @@ -7,16 +7,15 @@ import kotlinx.serialization.Serializable @Serializable data class RoomListDto( @SerialName("roomId") val roomId: Int, - @SerialName("imageUrl") val imageUrl: String?, - @SerialName("bookTitle") val bookTitle: String, + @SerialName("bookImageUrl") val bookImageUrl: String?, + @SerialName("roomName") val roomName: String, + @SerialName("recruitCount") val recruitCount: Int, @SerialName("memberCount") val memberCount: Int, - @SerialName("userPercentage") val userPercentage: Float, @SerialName("deadlineDate") val deadlineDate: String ) @Serializable data class RoomsHomeDto( - @SerialName("deadlineRoomList") val deadline: List = emptyList(), - @SerialName("popularityRoomList") val popularity: List = emptyList(), - @SerialName("influencerRoomList") val influencer: List = emptyList() + @SerialName("deadlineRoomList") val deadlineRoomList: List = emptyList(), + @SerialName("popularRoomList") val popularRoomList: List = emptyList() ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 6645edc5..de89e84d 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -99,21 +99,14 @@ class GroupRepository @Inject constructor( val sections = listOf( GroupRoomSectionData( title = context.getString(R.string.room_section_deadline), - rooms = roomsData.deadline.map { dto -> + rooms = roomsData.deadlineRoomList.map { dto -> convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) }, genres = genres ), GroupRoomSectionData( title = context.getString(R.string.room_section_popular), - rooms = roomsData.popularity.map { dto -> - convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) - }, - genres = genres - ), - GroupRoomSectionData( - title = context.getString(R.string.room_section_influencer), - rooms = roomsData.influencer.map { dto -> + rooms = roomsData.popularRoomList.map { dto -> convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) }, genres = genres @@ -170,9 +163,9 @@ class GroupRepository @Inject constructor( private fun convertToGroupCardItemRoomData(dto: RoomListDto, daysLeft: Int): GroupCardItemRoomData { return GroupCardItemRoomData( id = dto.roomId, - title = dto.bookTitle, + title = dto.roomName, participants = dto.memberCount, - maxParticipants = dto.memberCount + 10, // API에서 maxParticipants를 제공하지 않으므로 임시로 +10 + maxParticipants = dto.recruitCount, isRecruiting = true, endDate = daysLeft, imageRes = null, @@ -303,22 +296,7 @@ class GroupRepository @Inject constructor( else -> GroupBottomButtonType.JOIN // 참여하지 않았으면 참여 가능 } } - - suspend fun searchRooms(query: String): Result> { - return try { - val searchResult = getSearchGroups() - if (searchResult.isSuccess) { - val filteredRooms = searchResult.getOrThrow().filter { room -> - room.title.contains(query, ignoreCase = true) - } - Result.success(filteredRooms) - } else { - searchResult - } - } catch (e: Exception) { - Result.failure(e) - } - } + private fun initializeRoomDetail(room: GroupCardItemRoomData) { val bookData = GroupBookData( From 5c4f770781b392089ca335b19914b8e9fba81aaf Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 17:58:24 +0900 Subject: [PATCH 20/68] =?UTF-8?q?[feat]:=20Group=20Room=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=95=A8=EC=88=98=20=EC=82=AD=EC=A0=9C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigator/navigations/GroupNavigation.kt | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index e6adb926..f0d95111 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -1,12 +1,8 @@ package com.texthip.thip.ui.navigator.navigations import android.annotation.SuppressLint -import androidx.compose.runtime.LaunchedEffect 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.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController @@ -15,7 +11,6 @@ import androidx.navigation.toRoute import com.texthip.thip.ui.group.done.Screen.GroupDoneScreen import com.texthip.thip.ui.group.makeroom.screen.GroupMakeRoomScreen import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.screen.GroupMyScreen import com.texthip.thip.ui.group.room.screen.GroupRoomRecruitScreen import com.texthip.thip.ui.group.room.screen.GroupRoomScreen @@ -85,7 +80,6 @@ fun NavGraphBuilder.groupNavigation( // Group Done 화면 composable { - val groupViewModel: GroupViewModel = hiltViewModel() GroupDoneScreen( onNavigateBack = { navigateBack() @@ -163,21 +157,11 @@ fun NavGraphBuilder.groupNavigation( val route = backStackEntry.toRoute() val roomId = route.roomId val groupViewModel: GroupViewModel = hiltViewModel() - - // suspend 함수를 위한 LaunchedEffect 사용 - var roomDetail by remember { mutableStateOf(null) } - LaunchedEffect(roomId) { - roomDetail = groupViewModel.getRoomDetail(roomId) - } - - roomDetail?.let { - GroupRoomScreen( - onBackClick = { - navigateBack() - } - ) - } ?: run { - // 로딩 중이거나 데이터를 찾을 수 없는 경우 - } + + GroupRoomScreen( + onBackClick = { + navigateBack() + } + ) } } \ No newline at end of file From a5dc49ea19420942316fc9eddb446bf74c518414 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 17:59:35 +0900 Subject: [PATCH 21/68] =?UTF-8?q?[feat]:=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=9A=A9=20=ED=95=A8=EC=88=98=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/repository/GroupRepository.kt | 181 +----------------- .../thip/ui/group/viewmodel/GroupViewModel.kt | 19 +- 2 files changed, 14 insertions(+), 186 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index de89e84d..3f786da8 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -15,7 +15,6 @@ import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.delay import javax.inject.Inject import javax.inject.Singleton @@ -31,7 +30,6 @@ class GroupRepository @Inject constructor( context.getString(R.string.humanities), context.getString(R.string.art) ) - private val roomDetailsCache = mutableMapOf() private var cachedUserName: String? = null // UI 장르명 → API 카테고리명 매핑 @@ -112,13 +110,6 @@ class GroupRepository @Inject constructor( genres = genres ) ) - - // 상세 데이터 캐시에 저장 - val allRooms = sections.flatMap { it.rooms } - allRooms.forEach { room -> - initializeRoomDetail(room) - } - sections }.orEmpty() } @@ -138,6 +129,7 @@ class GroupRepository @Inject constructor( MyRoomCardData( roomId = room.roomId, bookImageUrl = room.bookImageUrl, + imageRes = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) bookTitle = room.bookTitle, memberCount = room.memberCount, endDate = room.endDate @@ -168,7 +160,8 @@ class GroupRepository @Inject constructor( maxParticipants = dto.recruitCount, isRecruiting = true, endDate = daysLeft, - imageRes = null, + imageRes = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) + imageUrl = dto.bookImageUrl, // API에서 받은 실제 이미지 URL genreIndex = 0, isSecret = false ) @@ -183,27 +176,6 @@ class GroupRepository @Inject constructor( } } - suspend fun getMyRoomGroups(): Result> { - return try { - val myRoomGroups = listOf( - GroupCardItemRoomData(23, "호르몬 체인지 완독하는 방", 22, 30, true, 5, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(24, "명작 읽기방", 10, 20, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(25, "또 다른 방", 13, 25, false, 10, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(26, "내가 참여한 과학책방", 18, 25, true, 7, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(27, "인문학 토론방", 12, 20, true, 2, R.drawable.bookcover_sample, 3) - ) - - // 상세 데이터 캐시에 저장 - myRoomGroups.forEach { room -> - initializeRoomDetail(room) - } - - Result.success(myRoomGroups) - } catch (e: Exception) { - Result.failure(e) - } - } - suspend fun getSearchGroups(): Result> { return try { // 기존에 로드된 섹션 데이터들을 합쳐서 반환 @@ -219,20 +191,6 @@ class GroupRepository @Inject constructor( } } - suspend fun getRoomDetail(roomId: Int): Result { - return try { - delay(150) - val roomDetail = roomDetailsCache[roomId] - if (roomDetail != null) { - Result.success(roomDetail) - } else { - Result.failure(Exception("Room not found: $roomId")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - // 모집중인 모임방 상세 정보 API 연동 suspend fun getRoomRecruiting(roomId: Int): Result { return try { @@ -245,7 +203,9 @@ class GroupRepository @Inject constructor( title = data.bookTitle, author = data.authorName, publisher = "출판사 정보 없음", // API에서 제공하지 않음 - description = data.bookDescription + description = data.bookDescription, + imageRes = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) + imageUrl = data.bookImageUrl // API에서 받은 실제 이미지 URL ) // 추천 모임방 변환 @@ -257,7 +217,8 @@ class GroupRepository @Inject constructor( maxParticipants = recommendDto.recruitCount, isRecruiting = true, endDate = extractDaysFromDeadline(recommendDto.recruitEndDate), - imageRes = null, // 이미지는 URL로 처리 + imageRes = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) + imageUrl = recommendDto.roomImageUrl, // API에서 받은 실제 이미지 URL genreIndex = 0, // 기본값 isSecret = true // 기본값 ) @@ -297,130 +258,4 @@ class GroupRepository @Inject constructor( } } - - private fun initializeRoomDetail(room: GroupCardItemRoomData) { - val bookData = GroupBookData( - title = "심장보다 단단한 토마토 한 알", - author = "고선지", - publisher = "푸른출판사", - description = "${room.title}에서 읽는 책입니다. 감동적인 이야기로 가득한 작품입니다.", - imageRes = room.imageRes ?: R.drawable.bookcover_sample - ) - - val recommendations = getRecommendations(room.id) - - val roomDetail = GroupRoomData( - id = room.id, - title = room.title, - isSecret = room.isSecret, - description = "${room.title} 모임입니다. 함께 책을 읽고 토론해요.", - startDate = "2025.01.12", - endDate = "2025.02.12", - members = room.participants, - maxMembers = room.maxParticipants, - daysLeft = room.endDate ?: 0, - genre = genres[room.genreIndex], - bookData = bookData, - recommendations = recommendations - ) - - roomDetailsCache[room.id] = roomDetail - - // 추천 모임방들의 상세 정보도 캐시에 추가 - recommendations.forEach { recommendedRoom -> - if (!roomDetailsCache.containsKey(recommendedRoom.id)) { - initializeRecommendedRoomDetail(recommendedRoom) - } - } - } - - // 추천 모임방 예시 by gpt - private fun initializeRecommendedRoomDetail(room: GroupCardItemRoomData) { - val bookTitles = listOf( - "데미안", "1984", "노인과 바다", "위대한 개츠비", "햄릿", - "코스모스", "이기적 유전자", "블랙홀과 시간여행", "총균쇠", - "국부론", "자본론", "사피엔스", "총균쇠", "정의란 무엇인가", - "예술의 역사", "음악의 역사", "미학 오디세이" - ) - - val authors = listOf( - "헤르만 헤세", "조지 오웰", "어니스트 헤밍웨이", "스콧 피츠제럴드", - "칼 세이건", "리처드 도킨스", "킵 손", "재레드 다이아몬드", - "아담 스미스", "칼 마르크스", "유발 하라리", "마이클 샌델" - ) - - val publishers = listOf("푸른출판사", "문학동네", "민음사", "창비", "열린책들", "김영사") - - val bookData = GroupBookData( - title = bookTitles.random(), - author = authors.random(), - publisher = publishers.random(), - description = "${room.title}에서 읽는 흥미로운 책입니다. 함께 읽으며 깊이 있는 토론을 나눠보세요.", - imageRes = room.imageRes ?: R.drawable.bookcover_sample - ) - - val roomDetail = GroupRoomData( - id = room.id, - title = room.title, - isSecret = room.isSecret, - description = "${room.title} 모임입니다. 다양한 관점으로 책을 읽고 의견을 나눠보세요.", - startDate = "2025.01.15", - endDate = "2025.02.15", - members = room.participants, - maxMembers = room.maxParticipants, - daysLeft = room.endDate ?: 0, - genre = genres.getOrElse(room.genreIndex) { genres[0] }, - bookData = bookData, - recommendations = getRecommendations(room.id) // 추천 모임방에도 추천 제공 - ) - - roomDetailsCache[room.id] = roomDetail - } - - private fun getRecommendations(roomId: Int): List { - // 추천 모임방 더미데이터 풀 - val recommendationPool = listOf( - // 문학 관련 추천 - GroupCardItemRoomData(1001, "한국 근현대 소설 읽기", 18, 25, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1002, "일본 문학 애호가들", 22, 30, true, 1, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1003, "시 읽기 모임", 16, 25, true, 2, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1004, "해외문학 번역서 읽기", 15, 22, true, 3, R.drawable.bookcover_sample, 0, true), - GroupCardItemRoomData(1005, "고전 문학 탐구", 20, 25, true, 5, R.drawable.bookcover_sample, 0), - - // 과학·IT 관련 추천 - GroupCardItemRoomData(1006, "SF 소설 탐험대", 12, 20, true, 7, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(1007, "과학도서 함께 읽기", 7, 15, true, 9, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(1008, "컴퓨터 과학 스터디", 14, 18, true, 4, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(1009, "물리학 입문서 모임", 10, 16, true, 6, R.drawable.bookcover_sample, 1), - - // 사회과학 관련 추천 - GroupCardItemRoomData(1010, "경제경영서 스터디", 9, 12, true, 6, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(1011, "사회학 도서 토론", 13, 18, true, 4, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(1012, "정치학 입문 모임", 11, 15, true, 8, R.drawable.bookcover_sample, 2), - - // 인문학 관련 추천 - GroupCardItemRoomData(1013, "철학 에세이 읽기 모임", 8, 15, true, 5, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(1014, "인문학 고전 읽기", 20, 25, true, 5, R.drawable.bookcover_sample, 3, true), - GroupCardItemRoomData(1015, "심리학 도서 스터디", 10, 16, true, 7, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(1016, "역사서 탐구 모임", 11, 16, true, 8, R.drawable.bookcover_sample, 3), - - // 예술 관련 추천 - GroupCardItemRoomData(1017, "미술사 도서 읽기", 14, 20, true, 3, R.drawable.bookcover_sample, 4), - GroupCardItemRoomData(1018, "음악 관련 서적 모임", 12, 18, true, 5, R.drawable.bookcover_sample, 4), - - // 기타 장르 - GroupCardItemRoomData(1019, "로맨스 소설 감상회", 14, 20, true, 4, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1020, "미스터리 소설 동호회", 15, 18, true, 2, R.drawable.bookcover_sample, 0, true), - GroupCardItemRoomData(1021, "자기계발서 함께 읽기", 25, 30, true, 3, R.drawable.bookcover_sample, 2, true), - GroupCardItemRoomData(1022, "판타지 소설 동호회", 24, 30, true, 1, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1023, "여행 에세이 모임", 13, 18, true, 4, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(1024, "추리소설 마니아", 19, 24, true, 6, R.drawable.bookcover_sample, 0) - ) - - // 현재 방과 관련 없는 추천을 제공하기 위해 현재 roomId와 다른 것들만 필터링 - val filteredRecommendations = recommendationPool.filter { it.id != roomId } - - // 랜덤하게 3-5개의 추천 반환 - return filteredRecommendations.shuffled().take(5) - } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index c333c54e..1e0cba1f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -163,10 +163,9 @@ class GroupViewModel @Inject constructor( private fun loadMyRoomGroups() { viewModelScope.launch { - repository.getMyRoomGroups() - .onSuccess { groups -> - _myRoomGroups.value = groups - } + // getMyRoomGroups() 제거됨 - API 연결 완료 후 실제 API 사용 예정 + // 현재는 빈 리스트로 설정 + _myRoomGroups.value = emptyList() } } @@ -237,10 +236,8 @@ class GroupViewModel @Inject constructor( } }, async { - repository.getMyRoomGroups() - .onSuccess { groups -> - _myRoomGroups.value = groups - } + // getMyRoomGroups() 제거됨 - API 연결 완료 후 실제 API 사용 예정 + _myRoomGroups.value = emptyList() } ) @@ -250,11 +247,7 @@ class GroupViewModel @Inject constructor( } } } - - - suspend fun getRoomDetail(roomId: Int): GroupRoomData? { - return repository.getRoomDetail(roomId).getOrNull() - } + suspend fun getRoomRecruiting(roomId: Int): Result { return repository.getRoomRecruiting(roomId) From 9f6c346337277c5c23df3f9d30e1ff657d8eaa8a Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 18:01:12 +0900 Subject: [PATCH 22/68] =?UTF-8?q?[feat]:=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/common/cards/CardItemRoomSmall.kt | 3 ++- .../texthip/thip/ui/common/cards/CardRoomBook.kt | 3 ++- .../thip/ui/group/done/mock/MyRoomCardData.kt | 5 ++++- .../thip/ui/group/myroom/mock/GroupBookData.kt | 3 ++- .../thip/ui/group/myroom/mock/GroupCardData.kt | 3 ++- .../ui/group/myroom/mock/GroupCardItemRoomData.kt | 3 ++- .../ui/group/room/screen/GroupRoomRecruitScreen.kt | 9 +++++---- .../ui/group/search/screen/GroupSearchScreen.kt | 14 +++++++------- 8 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt index e4599b5e..5f32c573 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt @@ -40,7 +40,8 @@ fun CardItemRoomSmall( participants: Int, maxParticipants: Int, endDate: Int?, - imageRes: Int? = R.drawable.bookcover_sample_small, + imageRes: Int? = R.drawable.bookcover_sample_small, // 스켈레톤 이미지 (fallback) + imageUrl: String? = null, // API에서 받은 이미지 URL isWide: Boolean = false, isSecret: Boolean = false, onClick: () -> Unit = {} diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt index b298e218..f7f08cc3 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt @@ -41,7 +41,8 @@ fun CardRoomBook( author: String, publisher: String, description: String, - imageRes: Int? = R.drawable.bookcover_sample, + imageRes: Int? = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) + imageUrl: String? = null, // API에서 받은 이미지 URL onClick: () -> Unit = {} ) { Card( diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt index 3c22b8be..3d7de95d 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -1,8 +1,11 @@ package com.texthip.thip.ui.group.done.mock +import com.texthip.thip.R + data class MyRoomCardData( val roomId: Int, - val bookImageUrl: String, + val bookImageUrl: String?, // API에서 받은 이미지 URL + val imageRes: Int = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) val bookTitle: String, val memberCount: Int, val endDate: String? = null diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt index ee24be4c..670502ea 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt @@ -7,5 +7,6 @@ data class GroupBookData( val author: String, val publisher: String, val description: String, - val imageRes: Int = R.drawable.bookcover_sample + val imageRes: Int = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) + val imageUrl: String? = null // API에서 받은 이미지 URL ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt index fbbd3b3c..f04922f8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt @@ -6,7 +6,8 @@ data class GroupCardData( val id: Int, val title: String, val members: Int, - val imageUrl: String?, // 추가 + val imageUrl: String?, // API에서 받은 이미지 URL + val imageRes: Int = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) val progress: Int, // 0~100 val nickname: String ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt index 00a64c30..ac76bb08 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt @@ -9,7 +9,8 @@ data class GroupCardItemRoomData( val maxParticipants: Int, val isRecruiting: Boolean, val endDate: Int? = null, // 남은 일 수 - val imageRes: Int? = R.drawable.bookcover_sample, + val imageRes: Int? = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) + val imageUrl: String? = null, // API에서 받은 이미지 URL val genreIndex: Int, // 장르 인덱스 val isSecret: Boolean = false ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index b73c8d4a..68a0c94b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.viewmodel.compose.viewModel import com.texthip.thip.R import com.texthip.thip.ui.common.cards.CardItemRoomSmall import com.texthip.thip.ui.common.cards.CardRoomBook @@ -67,10 +66,10 @@ fun GroupRoomRecruitScreen( ) { val context = LocalContext.current - var roomDetail by remember { mutableStateOf(mockRoomDetail) } + var roomDetail by remember { mutableStateOf(mockRoomDetail) } var isLoading by remember { mutableStateOf(mockRoomDetail == null && viewModel != null) } - var currentButtonType by remember { mutableStateOf(mockRoomDetail?.buttonType) } + var currentButtonType by remember { mutableStateOf(mockRoomDetail?.buttonType) } var showToast by remember { mutableStateOf(false) } var toastMessage by remember { mutableStateOf("") } var showDialog by remember { mutableStateOf(false) } @@ -330,7 +329,8 @@ fun GroupRoomRecruitScreen( author = detail.bookData.author, publisher = detail.bookData.publisher, description = detail.bookData.description, - imageRes = detail.bookData.imageRes + imageRes = detail.bookData.imageRes, + imageUrl = detail.bookData.imageUrl ) // 추천 모임방이 있을 때만 표시 @@ -356,6 +356,7 @@ fun GroupRoomRecruitScreen( maxParticipants = rec.maxParticipants, endDate = rec.endDate, imageRes = rec.imageRes, + imageUrl = rec.imageUrl, onClick = { onRecommendationClick(rec) } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt index bfcaedb8..ea2729cb 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt @@ -242,13 +242,13 @@ fun PreviewGroupSearchScreen() { ThipTheme { GroupSearchScreen( roomList = listOf( - GroupCardItemRoomData(1, "aaa", 22, 30, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(2, "abc", 15, 20, true, 7, R.drawable.bookcover_sample, 1, true), - GroupCardItemRoomData(3, "abcd", 10, 15, true, 5, R.drawable.bookcover_sample, 2, true), - GroupCardItemRoomData(4, "abcde", 8, 12, false, 2, R.drawable.bookcover_sample, 3, true), - GroupCardItemRoomData(5, "abcdef", 18, 25, true, 4, R.drawable.bookcover_sample, 4), - GroupCardItemRoomData(6, "abcdefg", 12, 20, true, 1, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(7, "abcdefgh", 10, 14, true, 6, R.drawable.bookcover_sample, 1) + GroupCardItemRoomData(1, "aaa", 22, 30, true, 3, R.drawable.bookcover_sample, "",0), + GroupCardItemRoomData(2, "abc", 15, 20, true, 7, R.drawable.bookcover_sample, "",1, true), + GroupCardItemRoomData(3, "abcd", 10, 15, true, 5, R.drawable.bookcover_sample, "",2, true), + GroupCardItemRoomData(4, "abcde", 8, 12, false, 2, R.drawable.bookcover_sample, "",3, true), + GroupCardItemRoomData(5, "abcdef", 18, 25, true, 4, R.drawable.bookcover_sample, "",4), + GroupCardItemRoomData(6, "abcdefg", 12, 20, true, 1, R.drawable.bookcover_sample, "",0), + GroupCardItemRoomData(7, "abcdefgh", 10, 14, true, 6, R.drawable.bookcover_sample, "",1) ) ) } From 7c5bf8e0bc9354789571c836189ad79b406d4dee Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 19:56:32 +0900 Subject: [PATCH 23/68] =?UTF-8?q?[feat]:=20=ED=86=A0=ED=81=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/texthip/thip/data/network/AuthInterceptor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt b/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt index e1a506f8..44c68111 100644 --- a/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt +++ b/app/src/main/java/com/texthip/thip/data/network/AuthInterceptor.kt @@ -12,7 +12,7 @@ class AuthInterceptor @Inject constructor() : Interceptor { // - SharedPreferences나 DataStore에서 토큰 읽기 // - 토큰 만료 시 자동 갱신 // - 로그인/로그아웃 상태 관리 - private val hardcodedToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDI4MjMzNiwiZXhwIjoxNzU2ODc0MzM2fQ.NG_xDSdh8A6egIX2EAFtsqDO4lmFphTzqgzHC-r8eXY" + private val hardcodedToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjYsImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.yu0NG_lUYbALflzRfVgpxxddNSvTf9H4YehbV3yUeec" override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() From 574c79c1396614ae0df7aa956e1d86c12437625a Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 20:01:12 +0900 Subject: [PATCH 24/68] =?UTF-8?q?[feat]:=20imageRes=20->=20imageUrl?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/repository/GroupRepository.kt | 4 -- .../thip/ui/common/cards/CardBookList.kt | 31 ++++---------- .../thip/ui/common/cards/CardBookSearch.kt | 26 ++++-------- .../thip/ui/common/cards/CardItemRoom.kt | 35 ++++++---------- .../thip/ui/common/cards/CardItemRoomSmall.kt | 20 ++++------ .../thip/ui/common/cards/CardRoomBook.kt | 23 ++++------- .../thip/ui/group/done/mock/MyRoomCardData.kt | 1 - .../component/GroupBookListWithScrollbar.kt | 4 +- .../makeroom/component/GroupSelectBook.kt | 12 +++--- .../ui/group/makeroom/mock/GroupBookData.kt | 34 ++++++++-------- .../component/GroupDeadlineRoomSection.kt | 2 +- .../ui/group/myroom/mock/GroupBookData.kt | 1 - .../ui/group/myroom/mock/GroupCardData.kt | 1 - .../myroom/mock/GroupCardItemRoomData.kt | 1 - .../ui/group/myroom/screen/GroupMyScreen.kt | 2 +- .../room/screen/GroupRoomRecruitScreen.kt | 8 ++-- .../component/GroupFilteredSearchResult.kt | 6 +-- .../search/component/GroupLiveSearchResult.kt | 8 ++-- .../group/search/screen/GroupSearchScreen.kt | 40 +++++++++++++++---- .../ui/mypage/component/MypageSaveBook.kt | 2 +- .../ui/search/component/SearchActiveField.kt | 2 +- .../component/SearchBookFilteredResult.kt | 2 +- .../ui/search/component/SearchRecentBook.kt | 8 ++-- .../texthip/thip/ui/search/mock/BookData.kt | 2 +- .../ui/search/screen/SearchBookGroupScreen.kt | 2 +- .../thip/ui/search/screen/SearchBookScreen.kt | 38 +++++++++--------- 26 files changed, 143 insertions(+), 172 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 3f786da8..757c474b 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -129,7 +129,6 @@ class GroupRepository @Inject constructor( MyRoomCardData( roomId = room.roomId, bookImageUrl = room.bookImageUrl, - imageRes = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) bookTitle = room.bookTitle, memberCount = room.memberCount, endDate = room.endDate @@ -160,7 +159,6 @@ class GroupRepository @Inject constructor( maxParticipants = dto.recruitCount, isRecruiting = true, endDate = daysLeft, - imageRes = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) imageUrl = dto.bookImageUrl, // API에서 받은 실제 이미지 URL genreIndex = 0, isSecret = false @@ -204,7 +202,6 @@ class GroupRepository @Inject constructor( author = data.authorName, publisher = "출판사 정보 없음", // API에서 제공하지 않음 description = data.bookDescription, - imageRes = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) imageUrl = data.bookImageUrl // API에서 받은 실제 이미지 URL ) @@ -217,7 +214,6 @@ class GroupRepository @Inject constructor( maxParticipants = recommendDto.recruitCount, isRecruiting = true, endDate = extractDaysFromDeadline(recommendDto.recruitEndDate), - imageRes = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) imageUrl = recommendDto.roomImageUrl, // API에서 받은 실제 이미지 URL genreIndex = 0, // 기본값 isSecret = true // 기본값 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 864ca8ef..87c8be00 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 @@ -1,20 +1,15 @@ package com.texthip.thip.ui.common.cards -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.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 @@ -23,13 +18,11 @@ 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.painterResource -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 +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -40,7 +33,7 @@ fun CardBookList( title: String, author: String, publisher: String, - imageRes: Int? = R.drawable.bookcover_sample, // 기본 이미지 리소스 + imageUrl: String? = null, // API에서 받은 이미지 URL isBookmarked: Boolean = false, onBookmarkClick: () -> Unit = {} ) { @@ -50,20 +43,12 @@ fun CardBookList( .background(Color.Transparent), ) { // 책 이미지 - Box( - modifier = Modifier - .size(width = 80.dp, height = 108.dp) - ) { - - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } + AsyncImage( + model = imageUrl ?: R.drawable.bookcover_sample, + contentDescription = "책 이미지", + modifier = Modifier.size(width = 80.dp, height = 108.dp), + contentScale = ContentScale.Crop + ) Spacer(modifier = Modifier.width(12.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt index 3f66b388..d431ca07 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt @@ -1,13 +1,10 @@ package com.texthip.thip.ui.common.cards -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.padding import androidx.compose.foundation.layout.size @@ -17,9 +14,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -30,7 +27,7 @@ fun CardBookSearch( modifier: Modifier = Modifier, number: Int? = null, title: String, - imageRes: Int? = R.drawable.bookcover_sample, // 기본 이미지 리소스 + imageUrl: String? = null, // API에서 받은 이미지 URL onClick: () -> Unit = {} ) { Row( @@ -49,19 +46,12 @@ fun CardBookSearch( } // 이미지 - Box( - modifier = Modifier - .size(width = 45.dp, height = 60.dp) - ) { - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } + AsyncImage( + model = imageUrl ?: R.drawable.bookcover_sample, + contentDescription = "책 이미지", + modifier = Modifier.size(width = 45.dp, height = 60.dp), + contentScale = ContentScale.Crop + ) Spacer(modifier = Modifier.width(8.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt index c319d0d1..ce4cb3d7 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt @@ -1,14 +1,11 @@ package com.texthip.thip.ui.common.cards -import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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 @@ -29,6 +26,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -42,7 +40,7 @@ fun CardItemRoom( maxParticipants: Int, isRecruiting: Boolean, endDate: Int? = null, - imageRes: Int? = R.drawable.bookcover_sample, + imageUrl: String? = null, hasBorder: Boolean = false, onClick: () -> Unit = {} ) { @@ -74,19 +72,12 @@ fun CardItemRoom( modifier = Modifier.fillMaxWidth() ) { // 이미지 - Box( - modifier = Modifier - .size(width = 80.dp, height = 107.dp) - ) { - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } + AsyncImage( + model = imageUrl ?: R.drawable.bookcover_sample, + contentDescription = "책 이미지", + modifier = Modifier.size(width = 80.dp, height = 107.dp), + contentScale = ContentScale.Crop + ) Spacer(modifier = Modifier.width(12.dp)) @@ -190,7 +181,7 @@ fun CardItemRoomPreview() { maxParticipants = 30, isRecruiting = true, endDate = 3, - imageRes = R.drawable.bookcover_sample + imageUrl = null ) CardItemRoom( title = "모임방 이름입니다. 모임방 이름입니다.", @@ -198,7 +189,7 @@ fun CardItemRoomPreview() { maxParticipants = 30, isRecruiting = false, endDate = 3, - imageRes = R.drawable.bookcover_sample + imageUrl = null ) CardItemRoom( title = "모임방 이름입니다. 모임방 이름입니다.", @@ -206,7 +197,7 @@ fun CardItemRoomPreview() { maxParticipants = 30, isRecruiting = true, endDate = 3, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, hasBorder = true ) CardItemRoom( @@ -215,7 +206,7 @@ fun CardItemRoomPreview() { maxParticipants = 30, isRecruiting = false, endDate = 3, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, hasBorder = true ) CardItemRoom( @@ -223,7 +214,7 @@ fun CardItemRoomPreview() { participants = 22, maxParticipants = 30, isRecruiting = false, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, hasBorder = true ) } diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt index 5f32c573..d123853b 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -40,7 +41,6 @@ fun CardItemRoomSmall( participants: Int, maxParticipants: Int, endDate: Int?, - imageRes: Int? = R.drawable.bookcover_sample_small, // 스켈레톤 이미지 (fallback) imageUrl: String? = null, // API에서 받은 이미지 URL isWide: Boolean = false, isSecret: Boolean = false, @@ -78,14 +78,12 @@ fun CardItemRoomSmall( modifier = Modifier .size(width = 60.dp, height = 80.dp) ) { - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } + AsyncImage( + model = imageUrl ?: R.drawable.bookcover_sample_small, + contentDescription = "책 이미지", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) if (isSecret) { Image( painter = painterResource(id = R.drawable.ic_secret_cover), @@ -169,8 +167,7 @@ fun CardItemRoomSmallPreview() { title = "방 제목입니다 방 제목입니다", participants = 22, maxParticipants = 30, - endDate = 3, - imageRes = R.drawable.bookcover_sample + endDate = 3 ) CardItemRoomSmall( @@ -178,7 +175,6 @@ fun CardItemRoomSmallPreview() { participants = 18, maxParticipants = 25, endDate = 5, - imageRes = R.drawable.bookcover_sample, isWide = true ) } diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt index f7f08cc3..79e10c68 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.common.cards -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -24,12 +23,12 @@ 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.painterResource import androidx.compose.ui.res.stringResource 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 +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -41,7 +40,6 @@ fun CardRoomBook( author: String, publisher: String, description: String, - imageRes: Int? = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) imageUrl: String? = null, // API에서 받은 이미지 URL onClick: () -> Unit = {} ) { @@ -89,19 +87,12 @@ fun CardRoomBook( modifier = Modifier.fillMaxWidth() ) { // 책 이미지 - Box( - modifier = Modifier - .size(width = 80.dp, height = 107.dp) - ) { - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } + AsyncImage( + model = imageUrl ?: R.drawable.bookcover_sample, + contentDescription = "책 이미지", + modifier = Modifier.size(width = 80.dp, height = 107.dp), + contentScale = ContentScale.Crop + ) Spacer(modifier = Modifier.width(16.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt index 3d7de95d..5825eadf 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -5,7 +5,6 @@ import com.texthip.thip.R data class MyRoomCardData( val roomId: Int, val bookImageUrl: String?, // API에서 받은 이미지 URL - val imageRes: Int = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) val bookTitle: String, val memberCount: Int, val endDate: String? = null diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt index db9fc7fb..e49ce2c8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt @@ -42,7 +42,7 @@ fun GroupBookListWithScrollbar( books.forEachIndexed { index, book -> CardBookSearch( title = book.title, - imageRes = book.imageRes, + imageUrl = book.imageUrl, onClick = { onBookClick(book) } ) @@ -67,7 +67,7 @@ fun PreviewBookListWithScrollbar() { ThipTheme { Column { GroupBookListWithScrollbar( - books = List(20) { BookData("Book $it", R.drawable.bookcover_sample) }, + books = List(20) { BookData("Book $it", null) }, onBookClick = {} ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt index dc9864fd..a16d9a48 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt @@ -1,7 +1,6 @@ package com.texthip.thip.ui.group.makeroom.component import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -19,10 +18,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.OptionChipButton import com.texthip.thip.ui.group.makeroom.mock.BookData @@ -83,12 +84,13 @@ fun GroupSelectBook( .height(80.dp), verticalAlignment = Alignment.Bottom ) { - Image( - painter = painterResource(selectedBook.imageRes), + AsyncImage( + model = selectedBook.imageUrl ?: R.drawable.bookcover_sample, contentDescription = selectedBook.title, modifier = Modifier .height(80.dp) - .width(60.dp) + .width(60.dp), + contentScale = ContentScale.Crop ) Spacer(modifier = Modifier.width(12.dp)) Column( @@ -128,7 +130,7 @@ fun GroupSelectBook( private val dummyBook = BookData( title = "호르몬 체인지", - imageRes = R.drawable.bookcover_sample, + imageUrl = null, author = "최정화" ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt index e80bd652..c8a023ec 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt @@ -4,27 +4,27 @@ import com.texthip.thip.R data class BookData( val title: String, - val imageRes: Int, // drawable 리소스 or 이미지 URL + val imageUrl: String? = null, // 이미지 URL val author: String? = null ) val dummySavedBooks = listOf( - BookData("토마토 컬러면", R.drawable.bookcover_sample, "최정화"), - BookData("사슴", R.drawable.bookcover_sample, "최정화"), - BookData("토마토 컬러면", R.drawable.bookcover_sample, "최정화"), - BookData("사슴", R.drawable.bookcover_sample, "최정화"), - BookData("토마토 컬러면", R.drawable.bookcover_sample, "최정화"), - BookData("사슴", R.drawable.bookcover_sample, "최정화"), - BookData("토마토 컬러면", R.drawable.bookcover_sample, "최정화"), - BookData("사슴", R.drawable.bookcover_sample, "최정화") + BookData("토마토 컬러면", null, "최정화"), + BookData("사슴", null, "최정화"), + BookData("토마토 컬러면", null, "최정화"), + BookData("사슴", null, "최정화"), + BookData("토마토 컬러면", null, "최정화"), + BookData("사슴", null, "최정화"), + BookData("토마토 컬러면", null, "최정화"), + BookData("사슴", null, "최정화") ) val dummyGroupBooks = listOf( - BookData("명작 읽기방", R.drawable.bookcover_sample), - BookData("또 다른 방", R.drawable.bookcover_sample), - BookData("명작 읽기방", R.drawable.bookcover_sample), - BookData("또 다른 방", R.drawable.bookcover_sample), - BookData("명작 읽기방", R.drawable.bookcover_sample), - BookData("또 다른 방", R.drawable.bookcover_sample), - BookData("명작 읽기방", R.drawable.bookcover_sample), - BookData("또 다른 방", R.drawable.bookcover_sample) + BookData("명작 읽기방", null), + BookData("또 다른 방", null), + BookData("명작 읽기방", null), + BookData("또 다른 방", null), + BookData("명작 읽기방", null), + BookData("또 다른 방", null), + BookData("명작 읽기방", null), + BookData("또 다른 방", null) ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt index 6ad0df14..c1c2eeac 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt @@ -199,7 +199,7 @@ fun GroupRoomDeadlineSection( maxParticipants = room.maxParticipants, isRecruiting = room.isRecruiting, endDate = room.endDate, - imageRes = room.imageRes, + imageUrl = room.imageUrl, onClick = { onRoomClick(room) }, hasBorder = true, ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt index 670502ea..17136442 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt @@ -7,6 +7,5 @@ data class GroupBookData( val author: String, val publisher: String, val description: String, - val imageRes: Int = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) val imageUrl: String? = null // API에서 받은 이미지 URL ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt index f04922f8..cbb6f718 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt @@ -7,7 +7,6 @@ data class GroupCardData( val title: String, val members: Int, val imageUrl: String?, // API에서 받은 이미지 URL - val imageRes: Int = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) val progress: Int, // 0~100 val nickname: String ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt index ac76bb08..4a76eed9 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt @@ -9,7 +9,6 @@ data class GroupCardItemRoomData( val maxParticipants: Int, val isRecruiting: Boolean, val endDate: Int? = null, // 남은 일 수 - val imageRes: Int? = R.drawable.bookcover_sample, // 스켈레톤 이미지 (fallback) val imageUrl: String? = null, // API에서 받은 이미지 URL val genreIndex: Int, // 장르 인덱스 val isSecret: Boolean = false diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index 2e436a72..efe3ce22 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -89,7 +89,7 @@ fun GroupMyScreen( maxParticipants = item.maxParticipants, isRecruiting = item.isRecruiting, endDate = item.endDate, - imageRes = item.imageRes, + imageUrl = item.imageUrl, onClick = { onCardClick(item) } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index 68a0c94b..b3c326c0 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -329,7 +329,6 @@ fun GroupRoomRecruitScreen( author = detail.bookData.author, publisher = detail.bookData.publisher, description = detail.bookData.description, - imageRes = detail.bookData.imageRes, imageUrl = detail.bookData.imageUrl ) @@ -355,7 +354,6 @@ fun GroupRoomRecruitScreen( participants = rec.participants, maxParticipants = rec.maxParticipants, endDate = rec.endDate, - imageRes = rec.imageRes, imageUrl = rec.imageUrl, onClick = { onRecommendationClick(rec) } ) @@ -531,7 +529,7 @@ fun GroupRoomRecruitScreenPreviewJoin() { author = "고선지", publisher = "푸른출판사", description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", - imageRes = R.drawable.bookcover_sample + imageUrl = null ) val detailJoin = GroupRoomData( @@ -601,7 +599,7 @@ fun GroupRoomRecruitScreenPreviewCancel() { author = "고선지", publisher = "푸른출판사", description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", - imageRes = R.drawable.bookcover_sample + imageUrl = null ) val detailCancel = GroupRoomData( @@ -671,7 +669,7 @@ fun GroupRoomRecruitScreenClose() { author = "고선지", publisher = "푸른출판사", description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", - imageRes = R.drawable.bookcover_sample + imageUrl = null ) val detailClose = GroupRoomData( diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt index ea039d5d..a7aaccae 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt @@ -78,7 +78,7 @@ fun GroupFilteredSearchResult( participants = room.participants, maxParticipants = room.maxParticipants, endDate = room.endDate, - imageRes = room.imageRes, + imageUrl = room.imageUrl, isWide = true, isSecret = room.isSecret, onClick = { onRoomClick(room) } @@ -121,7 +121,7 @@ fun GroupFilteredSearchResultPreview() { maxParticipants = 10, isRecruiting = true, endDate = 7, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, genreIndex = 1, isSecret = false ), GroupCardItemRoomData( @@ -131,7 +131,7 @@ fun GroupFilteredSearchResultPreview() { maxParticipants = 12, isRecruiting = false, endDate = 3, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, genreIndex = 1, isSecret = true ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt index 7674e39e..50c2f78b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt @@ -31,7 +31,7 @@ fun GroupLiveSearchResult( participants = room.participants, maxParticipants = room.maxParticipants, endDate = room.endDate, - imageRes = room.imageRes, + imageUrl = room.imageUrl, isWide = true, isSecret = room.isSecret, onClick = { onRoomClick(room) } @@ -66,7 +66,7 @@ fun GroupLiveSearchResultPreview() { maxParticipants = 10, isRecruiting = true, endDate = 7, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, genreIndex = 0, isSecret = false ), @@ -77,7 +77,7 @@ fun GroupLiveSearchResultPreview() { maxParticipants = 12, isRecruiting = false, endDate = 3, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, genreIndex = 1, isSecret = true ), @@ -88,7 +88,7 @@ fun GroupLiveSearchResultPreview() { maxParticipants = 8, isRecruiting = true, endDate = null, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, genreIndex = 2, isSecret = false ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt index ea2729cb..55c28132 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt @@ -242,13 +242,39 @@ fun PreviewGroupSearchScreen() { ThipTheme { GroupSearchScreen( roomList = listOf( - GroupCardItemRoomData(1, "aaa", 22, 30, true, 3, R.drawable.bookcover_sample, "",0), - GroupCardItemRoomData(2, "abc", 15, 20, true, 7, R.drawable.bookcover_sample, "",1, true), - GroupCardItemRoomData(3, "abcd", 10, 15, true, 5, R.drawable.bookcover_sample, "",2, true), - GroupCardItemRoomData(4, "abcde", 8, 12, false, 2, R.drawable.bookcover_sample, "",3, true), - GroupCardItemRoomData(5, "abcdef", 18, 25, true, 4, R.drawable.bookcover_sample, "",4), - GroupCardItemRoomData(6, "abcdefg", 12, 20, true, 1, R.drawable.bookcover_sample, "",0), - GroupCardItemRoomData(7, "abcdefgh", 10, 14, true, 6, R.drawable.bookcover_sample, "",1) + GroupCardItemRoomData( + id = 1, + title = "aaa", + participants = 22, + maxParticipants = 30, + isRecruiting = true, + endDate = 3, + imageUrl = null, + genreIndex = 0, + isSecret = false + ), + GroupCardItemRoomData( + id = 2, + title = "abc", + participants = 15, + maxParticipants = 20, + isRecruiting = true, + endDate = 7, + imageUrl = null, + genreIndex = 1, + isSecret = true + ), + GroupCardItemRoomData( + id = 3, + title = "abcd", + participants = 10, + maxParticipants = 15, + isRecruiting = true, + endDate = 5, + imageUrl = null, + genreIndex = 2, + isSecret = true + ) ) ) } diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/MypageSaveBook.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/MypageSaveBook.kt index bafb3dbf..f3cdd68f 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/MypageSaveBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/MypageSaveBook.kt @@ -18,7 +18,7 @@ fun BookContent(viewModel: SavedBookViewModel = viewModel()) { CardBookList( title = book.title, author = book.author, - imageRes = null, + imageUrl = null, publisher = book.publisher, isBookmarked = book.isSaved, onBookmarkClick = { viewModel.toggleBookmark(book.id) } diff --git a/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt b/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt index 088b582b..72d6f269 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt @@ -27,7 +27,7 @@ fun SearchActiveField( title = book.title, author = book.author, publisher = book.publisher, - imageRes = book.imageRes + imageUrl = book.imageUrl ) if (index < bookList.size - 1) { Spacer( diff --git a/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt b/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt index d3b130d0..95d150b4 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt @@ -63,7 +63,7 @@ fun SearchBookFilteredResult( title = book.title, author = book.author, publisher = book.publisher, - imageRes = book.imageRes + imageUrl = book.imageUrl ) if (index < bookList.size - 1) { Spacer( diff --git a/app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt b/app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt index 18db248c..c35cddc9 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt @@ -118,7 +118,7 @@ fun SearchRecentBook( CardBookSearch( number = index + 1, title = book.title, - imageRes = book.imageRes, + imageUrl = book.imageUrl, onClick = { onBookClick(book) } ) if (index < popularBooks.size - 1) { @@ -149,19 +149,19 @@ fun PreviewBookRecentSearch() { title = "이기적 유전자", author = "리처드 도킨스", publisher = "을유문화사", - imageRes = R.drawable.bookcover_sample + imageUrl = null ), BookData( title = "코스모스", author = "칼 세이건", publisher = "사이언스북스", - imageRes = R.drawable.bookcover_sample + imageUrl = null ), BookData( title = "총, 균, 쇠", author = "재레드 다이아몬드", publisher = "문학사상사", - imageRes = R.drawable.bookcover_sample + imageUrl = null ) ), popularBookDate = "01.12", diff --git a/app/src/main/java/com/texthip/thip/ui/search/mock/BookData.kt b/app/src/main/java/com/texthip/thip/ui/search/mock/BookData.kt index 0534fc31..b4610c61 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/mock/BookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/mock/BookData.kt @@ -6,5 +6,5 @@ data class BookData( val title: String, val author: String = "", val publisher: String = "", - val imageRes: Int = R.drawable.bookcover_sample + val imageUrl: String? = null ) diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt index c181a487..d3047851 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt @@ -109,7 +109,7 @@ fun SearchBookGroupScreen( maxParticipants = item.maxParticipants, isRecruiting = item.isRecruiting, endDate = item.endDate, - imageRes = item.imageRes, + imageUrl = item.imageUrl, onClick = { onCardClick(item) } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt index 7062aa3a..2eeb0f16 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt @@ -198,20 +198,20 @@ fun PreviewBookSearchScreen_Default() { ThipTheme { SearchBookScreen( bookList = listOf( - BookData("aaa", "리처드 도킨스", "을유문화사", R.drawable.bookcover_sample), - BookData("abc", "마틴 셀리그만", "물푸레", R.drawable.bookcover_sample), - BookData("abcd", "빅터 프랭클", "청림출판", R.drawable.bookcover_sample), - BookData("abcde", "칼 융", "문학과지성사", R.drawable.bookcover_sample), - BookData("abcdef", "에릭 프롬", "까치글방", R.drawable.bookcover_sample), - BookData("abcedfg", "알베르 카뮈", "민음사", R.drawable.bookcover_sample), - BookData("abcdefgh", "장 폴 사르트르", "문학동네", R.drawable.bookcover_sample), + BookData(title = "aaa", author = "리처드 도킨스", publisher = "을유문화사", imageUrl = null), + BookData(title = "abc", author = "마틴 셀리그만", publisher = "물푸레", imageUrl = null), + BookData(title = "abcd", author = "빅터 프랭클", publisher = "청림출판", imageUrl = null), + BookData(title = "abcde", author = "칼 융", publisher = "문학과지성사", imageUrl = null), + BookData(title = "abcdef", author = "에릭 프롬", publisher = "까치글방", imageUrl = null), + BookData(title = "abcedfg", author = "알베르 카뮈", publisher = "민음사", imageUrl = null), + BookData(title = "abcdefgh", author = "장 폴 사르트르", publisher = "문학동네", imageUrl = null), ), popularBooks = listOf( - BookData("단 한번의 삶", "리처드 도킨스", "을유문화사", R.drawable.bookcover_sample), - BookData("사랑", "마틴 셀리그만", "물푸레", R.drawable.bookcover_sample), - BookData("호모 사피엔스", "빅터 프랭클", "청림출판", R.drawable.bookcover_sample), - BookData("코스모스 실버", "칼 융", "문학과지성사", R.drawable.bookcover_sample), - BookData("오만과 편견", "에릭 프롬", "까치글방", R.drawable.bookcover_sample), + BookData(title = "단 한번의 삶", author = "리처드 도킨스", publisher = "을유문화사", imageUrl = null), + BookData(title = "사랑", author = "마틴 셀리그만", publisher = "물푸레", imageUrl = null), + BookData(title = "호모 사피엔스", author = "빅터 프랭클", publisher = "청림출판", imageUrl = null), + BookData(title = "코스모스 실버", author = "칼 융", publisher = "문학과지성사", imageUrl = null), + BookData(title = "오만과 편견", author = "에릭 프롬", publisher = "까치글방", imageUrl = null), ) ) } @@ -223,13 +223,13 @@ fun PreviewBookSearchScreen_EmptyPopular() { ThipTheme { SearchBookScreen( bookList = listOf( - BookData("aaa", "리처드 도킨스", "을유문화사", R.drawable.bookcover_sample), - BookData("abc", "마틴 셀리그만", "물푸레", R.drawable.bookcover_sample), - BookData("abcd", "빅터 프랭클", "청림출판", R.drawable.bookcover_sample), - BookData("abcde", "칼 융", "문학과지성사", R.drawable.bookcover_sample), - BookData("abcdef", "에릭 프롬", "까치글방", R.drawable.bookcover_sample), - BookData("abcedfg", "알베르 카뮈", "민음사", R.drawable.bookcover_sample), - BookData("abcdefgh", "장 폴 사르트르", "문학동네", R.drawable.bookcover_sample), + BookData(title = "aaa", author = "리처드 도킨스", publisher = "을유문화사", imageUrl = null), + BookData(title = "abc", author = "마틴 셀리그만", publisher = "물푸레", imageUrl = null), + BookData(title = "abcd", author = "빅터 프랭클", publisher = "청림출판", imageUrl = null), + BookData(title = "abcde", author = "칼 융", publisher = "문학과지성사", imageUrl = null), + BookData(title = "abcdef", author = "에릭 프롬", publisher = "까치글방", imageUrl = null), + BookData(title = "abcedfg", author = "알베르 카뮈", publisher = "민음사", imageUrl = null), + BookData(title = "abcdefgh", author = "장 폴 사르트르", publisher = "문학동네", imageUrl = null), ), popularBooks = emptyList() ) From 691ce6d541d61f8659540455557ab5022673ee92 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 20:13:09 +0900 Subject: [PATCH 25/68] =?UTF-8?q?[ui]:=20=EC=B1=85=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EB=B0=94=ED=85=80=20=EC=8B=9C=ED=8A=B8=20ui=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/common/forms/SearchBookTextField.kt | 2 +- .../component/GroupBookSearchBottomSheet.kt | 68 ++++++++++--------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt b/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt index 066ada9d..11233ceb 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt @@ -53,7 +53,7 @@ fun SearchBookTextField( .fillMaxWidth() .height(40.dp) .clip(shape) - .background(colors.DarkGrey), + .background(colors.DarkGrey02), contentAlignment = Alignment.CenterStart ) { Row( diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt index 829e1701..9d3afc1b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt @@ -57,6 +57,9 @@ fun GroupBookSearchBottomSheet( } } + // 검색 결과가 있는지 확인 + val hasSearchResults = searchText.isEmpty() || filteredBooks.isNotEmpty() + CustomBottomSheet( onDismiss = onDismiss ) { @@ -75,56 +78,55 @@ fun GroupBookSearchBottomSheet( Spacer(Modifier.height(20.dp)) } - if (hasBooks) { + // 책이 있고 검색 결과가 있을 때만 탭 표시 + if (hasBooks && hasSearchResults) { HeaderMenuBarTab( titles = tabs, selectedTabIndex = selectedTab, onTabSelected = { selectedTab = it - // searchText = "" }, indicatorColor = ThipTheme.colors.White, modifier = Modifier.fillMaxWidth() ) + } - Column( - Modifier - .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, bottom = 20.dp) - ) { - Spacer(Modifier.height(20.dp)) + Column( + Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, bottom = 90.dp) + ) { + Spacer(Modifier.height(20.dp)) - when { - currentBooks.isEmpty() -> { - EmptyBookSheetContent(onRequestBook = onRequestBook) - } - filteredBooks.isEmpty() && searchText.isNotEmpty() -> { - SearchEmptyContent( - searchText = searchText, - onRequestBook = onRequestBook - ) - } - else -> { - GroupBookListWithScrollbar( - books = filteredBooks, - onBookClick = onBookSelect - ) - } + when { + // 검색 결과가 없을 때 (검색어가 있지만 결과가 없음) + searchText.isNotEmpty() && filteredBooks.isEmpty() -> { + SearchEmptyContent( + searchText = searchText, + onRequestBook = onRequestBook + ) + } + // 전체 책이 없을 때 + !hasBooks -> { + EmptyBookSheetContent(onRequestBook = onRequestBook) + } + // 현재 탭의 책이 없을 때 + currentBooks.isEmpty() -> { + EmptyBookSheetContent(onRequestBook = onRequestBook) + } + // 정상적으로 책 목록이 있을 때 + else -> { + GroupBookListWithScrollbar( + books = filteredBooks, + onBookClick = onBookSelect + ) } - } - } else { - Column( - Modifier - .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, bottom = 20.dp) - ) { - Spacer(Modifier.height(20.dp)) - EmptyBookSheetContent(onRequestBook = onRequestBook) } } } } + // 검색 결과가 없을 때 표시할 컴포넌트 (필요시 구현) @Composable private fun SearchEmptyContent( From 4f85a0f14b5ff8a0934b965fd0a06e582ee0255b Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 20:38:09 +0900 Subject: [PATCH 26/68] =?UTF-8?q?[feat]:=20=EC=A0=80=EC=9E=A5=EB=90=9C/?= =?UTF-8?q?=EB=AA=A8=EC=9E=84=20=EC=B1=85=20=EC=A1=B0=ED=9A=8C=20dto=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/model/book/response/BookDto.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/book/response/BookDto.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/book/response/BookDto.kt b/app/src/main/java/com/texthip/thip/data/model/book/response/BookDto.kt new file mode 100644 index 00000000..da46d962 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/book/response/BookDto.kt @@ -0,0 +1,17 @@ +package com.texthip.thip.data.model.book.response + +import kotlinx.serialization.Serializable + +@Serializable +data class BookDto( + val isbn: String, + val bookTitle: String, + val authorName: String, + val publisher: String, + val imageUrl: String? +) + +@Serializable +data class BookListResponse( + val bookList: List +) \ No newline at end of file From da20f63460c95da6be4aad33e00079c39b39e2e0 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 20:38:41 +0900 Subject: [PATCH 27/68] =?UTF-8?q?[feat]:=20=EC=A0=80=EC=9E=A5=EB=90=9C/?= =?UTF-8?q?=EB=AA=A8=EC=9E=84=20=EC=B1=85=20=EC=A1=B0=ED=9A=8C=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4,=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/model/repository/GroupRepository.kt | 14 ++++++++++++++ .../thip/data/model/service/GroupService.kt | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 757c474b..342431f4 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -9,6 +9,7 @@ import com.texthip.thip.data.model.service.GroupService import com.texthip.thip.ui.group.done.mock.MyRoomCardData import com.texthip.thip.ui.group.done.mock.MyRoomsPaginationResult import com.texthip.thip.ui.group.myroom.mock.GroupBookData +import com.texthip.thip.data.model.book.response.BookDto import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData @@ -254,4 +255,17 @@ class GroupRepository @Inject constructor( } } + // 책 검색 API 연동 + suspend fun getBooks(type: String): Result> { + return try { + groupService.getBooks(type) + .handleBaseResponse() + .mapCatching { bookListResponse -> + bookListResponse?.bookList ?: emptyList() + } + } catch (e: Exception) { + Result.failure(e) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index 3fcc4485..d61a65f3 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -1,6 +1,7 @@ package com.texthip.thip.data.model.service import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.book.response.BookListResponse import com.texthip.thip.data.model.group.response.JoinedRoomsDto import com.texthip.thip.data.model.group.response.MyRoomsDto import com.texthip.thip.data.model.group.response.RoomRecruitingDto @@ -30,4 +31,9 @@ interface GroupService { @GET("rooms/{roomId}/recruiting") suspend fun getRoomRecruiting(@Path("roomId") roomId: Int): BaseResponse + @GET("books") + suspend fun getBooks( + @Query("type") type: String // "saved" 또는 "joining" + ): BaseResponse + } \ No newline at end of file From 66ff9526dfdfc2583d188ff97d18d63f6c00824e Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 20:39:47 +0900 Subject: [PATCH 28/68] =?UTF-8?q?[feat]:=20=EC=A0=80=EC=9E=A5=EB=90=9C/?= =?UTF-8?q?=EB=AA=A8=EC=9E=84=20=EC=B1=85=20=EC=A1=B0=ED=9A=8C=20viewModel?= =?UTF-8?q?=20Screen=EA=B3=BC=20=EC=97=B0=EA=B2=B0=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/GroupBookSearchBottomSheet.kt | 19 ++++--- .../makeroom/screen/GroupMakeRoomScreen.kt | 10 ++-- .../viewmodel/GroupMakeRoomViewModel.kt | 52 +++++++++++++++++++ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt index 9d3afc1b..67d8afe9 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt @@ -18,21 +18,22 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.texthip.thip.R +import com.texthip.thip.ui.common.bottomsheet.CustomBottomSheet import com.texthip.thip.ui.common.forms.SearchBookTextField +import com.texthip.thip.ui.common.header.HeaderMenuBarTab import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.group.makeroom.mock.dummyGroupBooks import com.texthip.thip.ui.group.makeroom.mock.dummySavedBooks import com.texthip.thip.ui.theme.ThipTheme -import com.texthip.thip.ui.common.bottomsheet.CustomBottomSheet -import com.texthip.thip.ui.common.header.HeaderMenuBarTab @Composable fun GroupBookSearchBottomSheet( onDismiss: () -> Unit, onBookSelect: (BookData) -> Unit, onRequestBook: () -> Unit, - savedBooks: List = emptyList(), - groupBooks: List = emptyList() + savedBooks: List, + groupBooks: List, + isLoading: Boolean = false ) { val hasBooks = savedBooks.isNotEmpty() || groupBooks.isNotEmpty() var selectedTab by rememberSaveable { mutableIntStateOf(0) } @@ -99,6 +100,10 @@ fun GroupBookSearchBottomSheet( Spacer(Modifier.height(20.dp)) when { + // 로딩 중 + isLoading -> { + EmptyBookSheetContent(onRequestBook = onRequestBook) + } // 검색 결과가 없을 때 (검색어가 있지만 결과가 없음) searchText.isNotEmpty() && filteredBooks.isEmpty() -> { SearchEmptyContent( @@ -149,7 +154,8 @@ fun PreviewBookSearchBottomSheet_HasBooks() { onBookSelect = {}, onRequestBook = {}, savedBooks = dummySavedBooks, // 데이터 있음 - groupBooks = dummyGroupBooks + groupBooks = dummyGroupBooks, + isLoading = false ) } } @@ -166,7 +172,8 @@ fun PreviewBookSearchBottomSheet_Empty() { onBookSelect = {}, onRequestBook = {}, savedBooks = emptyList(), // 데이터 없음 - groupBooks = emptyList() + groupBooks = emptyList(), + isLoading = false ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt index 3920a5f4..8174e4c8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt @@ -37,8 +37,6 @@ import com.texthip.thip.ui.group.makeroom.component.GroupSelectBook import com.texthip.thip.ui.group.makeroom.component.GroupMemberLimitPicker import com.texthip.thip.ui.group.makeroom.component.SectionDivider import com.texthip.thip.ui.group.makeroom.mock.BookData -import com.texthip.thip.ui.group.makeroom.mock.dummyGroupBooks -import com.texthip.thip.ui.group.makeroom.mock.dummySavedBooks import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -53,6 +51,9 @@ fun GroupMakeRoomScreen( modifier: Modifier = Modifier ) { val uiState by viewModel.uiState.collectAsState() + val savedBooks by viewModel.savedBooks.collectAsState() + val groupBooks by viewModel.groupBooks.collectAsState() + val isLoadingBooks by viewModel.isLoadingBooks.collectAsState() val scrollState = rememberScrollState() val genres = viewModel.genres @@ -212,8 +213,9 @@ fun GroupMakeRoomScreen( onRequestBook = { viewModel.toggleBookSearchSheet(false) }, - savedBooks = dummySavedBooks, - groupBooks = dummyGroupBooks + savedBooks = savedBooks, + groupBooks = groupBooks, + isLoading = isLoadingBooks ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index e20f96da..6888ec64 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -3,6 +3,7 @@ package com.texthip.thip.ui.group.makeroom.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.data.model.book.response.BookDto import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState import dagger.hilt.android.lifecycle.HiltViewModel @@ -21,6 +22,16 @@ class GroupMakeRoomViewModel @Inject constructor( private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) val uiState: StateFlow = _uiState.asStateFlow() + // 책 목록 상태 + private val _savedBooks = MutableStateFlow>(emptyList()) + val savedBooks: StateFlow> = _savedBooks.asStateFlow() + + private val _groupBooks = MutableStateFlow>(emptyList()) + val groupBooks: StateFlow> = _groupBooks.asStateFlow() + + private val _isLoadingBooks = MutableStateFlow(false) + val isLoadingBooks: StateFlow = _isLoadingBooks.asStateFlow() + val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") // 책 선택 @@ -31,6 +42,47 @@ class GroupMakeRoomViewModel @Inject constructor( // 책 검색 시트 표시 상태 변경 fun toggleBookSearchSheet(show: Boolean) { _uiState.value = _uiState.value.copy(showBookSearchSheet = show) + if (show) { + loadBooks() + } + } + + // 책 목록 로드 + private fun loadBooks() { + viewModelScope.launch { + _isLoadingBooks.value = true + try { + // 저장한 책 로드 + val savedBooksResult = groupRepository.getBooks("saved") + savedBooksResult.onSuccess { bookDtos -> + _savedBooks.value = bookDtos.map { it.toBookData() } + }.onFailure { + _savedBooks.value = emptyList() + } + + // 모임 책 로드 + val groupBooksResult = groupRepository.getBooks("joining") + groupBooksResult.onSuccess { bookDtos -> + _groupBooks.value = bookDtos.map { it.toBookData() } + }.onFailure { + _groupBooks.value = emptyList() + } + } catch (e: Exception) { + _savedBooks.value = emptyList() + _groupBooks.value = emptyList() + } finally { + _isLoadingBooks.value = false + } + } + } + + // BookDto를 BookData로 변환 + private fun BookDto.toBookData(): BookData { + return BookData( + title = this.bookTitle, + imageUrl = this.imageUrl, + author = this.authorName + ) } // 장르 선택 From e86901d03c6859fb426d5c2f67df7366a6a56096 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 20:56:06 +0900 Subject: [PATCH 29/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20DTO=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/group/request/CreateRoomRequest.kt | 16 ++++++++++++++++ .../model/group/response/CreateRoomResponse.kt | 8 ++++++++ 2 files changed, 24 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt create mode 100644 app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt b/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt new file mode 100644 index 00000000..6df73607 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt @@ -0,0 +1,16 @@ +package com.texthip.thip.data.model.group.request + +import kotlinx.serialization.Serializable + +@Serializable +data class CreateRoomRequest( + val isbn: String, + val category: String, + val roomName: String, + val description: String, + val progressStartDate: String, // yyyy.MM.dd 형식 + val progressEndDate: String, // yyyy.MM.dd 형식 + val recruitCount: Int, // 1~30명 + val password: String? = null, // 비공개방일 때만 필요 (숫자 4자리) + val isPublic: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt new file mode 100644 index 00000000..f681afe0 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.Serializable + +@Serializable +data class CreateRoomResponse( + val roomId: Int +) \ No newline at end of file From b16fbd41f64f7dcda7c74da44bf00bcc7464a60f Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 20:56:19 +0900 Subject: [PATCH 30/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4,=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EB=94=94=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80=20(#6?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/model/repository/GroupRepository.kt | 14 ++++++++++++++ .../thip/data/model/service/GroupService.kt | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 342431f4..f5f7170a 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -3,6 +3,7 @@ package com.texthip.thip.data.model.repository import android.content.Context import com.texthip.thip.R import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.data.model.group.response.PaginationResult import com.texthip.thip.data.model.group.response.RoomListDto import com.texthip.thip.data.model.service.GroupService @@ -268,4 +269,17 @@ class GroupRepository @Inject constructor( } } + // 모임방 생성 API 연동 + suspend fun createRoom(request: CreateRoomRequest): Result { + return try { + groupService.createRoom(request) + .handleBaseResponse() + .mapCatching { createRoomResponse -> + createRoomResponse?.roomId ?: throw Exception("방 생성 실패: roomId가 없습니다") + } + } catch (e: Exception) { + Result.failure(e) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index d61a65f3..35f5b2bb 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -2,11 +2,15 @@ package com.texthip.thip.data.model.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.book.response.BookListResponse +import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.model.group.response.CreateRoomResponse import com.texthip.thip.data.model.group.response.JoinedRoomsDto import com.texthip.thip.data.model.group.response.MyRoomsDto import com.texthip.thip.data.model.group.response.RoomRecruitingDto import com.texthip.thip.data.model.group.response.RoomsHomeDto +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -36,4 +40,9 @@ interface GroupService { @Query("type") type: String // "saved" 또는 "joining" ): BaseResponse + @POST("rooms") + suspend fun createRoom( + @Body request: CreateRoomRequest + ): BaseResponse + } \ No newline at end of file From 5929c86c63b8680358e3cb28c7f0d747b1e22abe Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 5 Aug 2025 20:59:58 +0900 Subject: [PATCH 31/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20viewModel,=20Screen=EA=B3=BC=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/makeroom/mock/GroupBookData.kt | 3 +- .../makeroom/screen/GroupMakeRoomScreen.kt | 10 +++- .../viewmodel/GroupMakeRoomViewModel.kt | 55 +++++++++++++++---- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt index c8a023ec..4ffad0bf 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt @@ -5,7 +5,8 @@ import com.texthip.thip.R data class BookData( val title: String, val imageUrl: String? = null, // 이미지 URL - val author: String? = null + val author: String? = null, + val isbn: String? = null // 방 생성 시 필요한 ISBN ) val dummySavedBooks = listOf( diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt index 8174e4c8..d06d6a50 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt @@ -78,8 +78,14 @@ fun GroupMakeRoomScreen( onLeftClick = onNavigateBack, onRightClick = { viewModel.createGroup( - onSuccess = onGroupCreated, - onError = { /* 에러는 uiState.errorMessage로 처리 */ } + onSuccess = { roomId -> + // TODO: 생성된 roomId를 사용하여 해당 방으로 이동할 수 있음 + onGroupCreated() + }, + onError = { errorMessage -> + // TODO: 에러 메시지 표시 (토스트 메시지 등) + // 현재는 uiState.errorMessage를 통해 처리 + } ) } ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index 6888ec64..0b09c069 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.repository.GroupRepository import com.texthip.thip.data.model.book.response.BookDto +import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,6 +13,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.time.LocalDate +import java.time.format.DateTimeFormatter import javax.inject.Inject @HiltViewModel @@ -81,7 +83,8 @@ class GroupMakeRoomViewModel @Inject constructor( return BookData( title = this.bookTitle, imageUrl = this.imageUrl, - author = this.authorName + author = this.authorName, + isbn = this.isbn ) } @@ -127,11 +130,17 @@ class GroupMakeRoomViewModel @Inject constructor( } // 그룹 생성 요청 - fun createGroup(onSuccess: () -> Unit, onError: (String) -> Unit) { + fun createGroup(onSuccess: (Int) -> Unit, onError: (String) -> Unit) { val currentState = _uiState.value if (!currentState.isFormValid) { - //onError("입력 정보를 확인해주세요") + onError("입력 정보를 확인해주세요") + return + } + + val selectedBook = currentState.selectedBook + if (selectedBook?.isbn == null) { + onError("책 정보가 올바르지 않습니다") return } @@ -139,19 +148,45 @@ class GroupMakeRoomViewModel @Inject constructor( try { _uiState.value = currentState.copy(isLoading = true, errorMessage = null) - // TODO: 실제 API 호출로 대체 - // val request = currentState.toRequest() - // val result = groupRepository.createGroup(request) - - // 임시로 성공 처리 - onSuccess() + // API 요청 데이터 생성 + val request = CreateRoomRequest( + isbn = selectedBook.isbn, + category = getApiCategoryName(currentState.selectedGenreIndex), + roomName = currentState.roomTitle.trim(), + description = currentState.roomDescription.trim(), + progressStartDate = currentState.meetingStartDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")), + progressEndDate = currentState.meetingEndDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")), + recruitCount = currentState.memberLimit, + password = if (currentState.isPrivate) currentState.password else null, + isPublic = !currentState.isPrivate + ) + + // API 호출 + val result = groupRepository.createRoom(request) + result.onSuccess { roomId -> + onSuccess(roomId) + }.onFailure { exception -> + onError("모임방 생성에 실패했습니다: ${exception.message}") + } } catch (e: Exception) { - //onError("네트워크 오류가 발생했습니다: ${e.message}") + onError("네트워크 오류가 발생했습니다: ${e.message}") } finally { _uiState.value = _uiState.value.copy(isLoading = false) } } } + + // 장르 인덱스를 API 카테고리명으로 변환 + private fun getApiCategoryName(genreIndex: Int): String { + return when (genreIndex) { + 0 -> "문학" + 1 -> "과학/IT" + 2 -> "사회과학" + 3 -> "인문학" + 4 -> "예술" + else -> "문학" // 기본값 + } + } // 에러 메시지 클리어 fun clearError() { From 029ec80da076f5c00d8eaf839a601042130b066c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Wed, 6 Aug 2025 17:25:04 +0900 Subject: [PATCH 32/68] =?UTF-8?q?[feat]:=20DTO=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20Book=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=9D=B4=EB=8F=99=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/di/ServiceModule.kt | 7 +++++ .../{BookDto.kt => BookSavedResponse.kt} | 4 +-- ...dRoomsDto.kt => JoinedRoomListResponse.kt} | 6 ++--- .../{MyRoomsDto.kt => MyRoomListResponse.kt} | 6 ++--- .../{RoomListDto.kt => RoomMainResponse.kt} | 8 +++--- ...uitingDto.kt => RoomRecruitingResponse.kt} | 6 ++--- .../data/model/repository/BookRepository.kt | 26 +++++++++++++++++++ .../data/model/repository/GroupRepository.kt | 20 +++----------- .../thip/data/model/service/BookService.kt | 14 ++++++++++ .../thip/data/model/service/GroupService.kt | 16 ++++++------ .../ui/group/makeroom/mock/GroupBookData.kt | 2 -- .../viewmodel/GroupMakeRoomViewModel.kt | 14 +++++----- 12 files changed, 81 insertions(+), 48 deletions(-) rename app/src/main/java/com/texthip/thip/data/model/book/response/{BookDto.kt => BookSavedResponse.kt} (79%) rename app/src/main/java/com/texthip/thip/data/model/group/response/{JoinedRoomsDto.kt => JoinedRoomListResponse.kt} (85%) rename app/src/main/java/com/texthip/thip/data/model/group/response/{MyRoomsDto.kt => MyRoomListResponse.kt} (82%) rename app/src/main/java/com/texthip/thip/data/model/group/response/{RoomListDto.kt => RoomMainResponse.kt} (84%) rename app/src/main/java/com/texthip/thip/data/model/group/response/{RoomRecruitingDto.kt => RoomRecruitingResponse.kt} (90%) create mode 100644 app/src/main/java/com/texthip/thip/data/model/repository/BookRepository.kt create mode 100644 app/src/main/java/com/texthip/thip/data/model/service/BookService.kt diff --git a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt index 1c0ec9d1..486ec97b 100644 --- a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt +++ b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt @@ -1,5 +1,6 @@ package com.texthip.thip.data.di +import com.texthip.thip.data.model.service.BookService import com.texthip.thip.data.model.service.GroupService import dagger.Module import dagger.Provides @@ -17,4 +18,10 @@ object ServiceModule { fun provideGroupService(retrofit: Retrofit): GroupService { return retrofit.create(GroupService::class.java) } + + @Provides + @Singleton + fun provideBookService(retrofit: Retrofit): BookService { + return retrofit.create(BookService::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/book/response/BookDto.kt b/app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt similarity index 79% rename from app/src/main/java/com/texthip/thip/data/model/book/response/BookDto.kt rename to app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt index da46d962..879a2282 100644 --- a/app/src/main/java/com/texthip/thip/data/model/book/response/BookDto.kt +++ b/app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt @@ -3,7 +3,7 @@ package com.texthip.thip.data.model.book.response import kotlinx.serialization.Serializable @Serializable -data class BookDto( +data class BookSavedResponse( val isbn: String, val bookTitle: String, val authorName: String, @@ -13,5 +13,5 @@ data class BookDto( @Serializable data class BookListResponse( - val bookList: List + val bookList: List ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt similarity index 85% rename from app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt rename to app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt index 225a5116..b5d109c1 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomsDto.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt @@ -5,8 +5,8 @@ import kotlinx.serialization.Serializable @Serializable -data class JoinedRoomsDto( - @SerialName("roomList") val roomList: List, +data class JoinedRoomListResponse( + @SerialName("roomList") val roomList: List, @SerialName("nickname") val nickname: String, @SerialName("page") val page: Int, @SerialName("size") val size: Int, @@ -15,7 +15,7 @@ data class JoinedRoomsDto( ) @Serializable -data class JoinedRoomDto( +data class JoinedRoomResponse( @SerialName("roomId") val roomId: Int, @SerialName("bookImageUrl") val bookImageUrl: String?, @SerialName("bookTitle") val bookTitle: String, diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomsDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt similarity index 82% rename from app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomsDto.kt rename to app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt index 5e8f0de4..e36aef9c 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomsDto.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt @@ -4,14 +4,14 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class MyRoomsDto( - @SerialName("roomList") val roomList: List, +data class MyRoomListResponse( + @SerialName("roomList") val roomList: List, @SerialName("nextCursor") val nextCursor: String?, @SerialName("isLast") val isLast: Boolean ) @Serializable -data class MyRoomDto( +data class MyRoomResponse( @SerialName("roomId") val roomId: Int, @SerialName("bookImageUrl") val bookImageUrl: String, @SerialName("bookTitle") val bookTitle: String, diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomMainResponse.kt similarity index 84% rename from app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt rename to app/src/main/java/com/texthip/thip/data/model/group/response/RoomMainResponse.kt index db3cd48e..a0b9bb51 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomListDto.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomMainResponse.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable -data class RoomListDto( +data class RoomMainResponse( @SerialName("roomId") val roomId: Int, @SerialName("bookImageUrl") val bookImageUrl: String?, @SerialName("roomName") val roomName: String, @@ -15,7 +15,7 @@ data class RoomListDto( ) @Serializable -data class RoomsHomeDto( - @SerialName("deadlineRoomList") val deadlineRoomList: List = emptyList(), - @SerialName("popularRoomList") val popularRoomList: List = emptyList() +data class RoomMainList( + @SerialName("deadlineRoomList") val deadlineRoomList: List = emptyList(), + @SerialName("popularRoomList") val popularRoomList: List = emptyList() ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingDto.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt similarity index 90% rename from app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingDto.kt rename to app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt index 5cd95c53..81fc9bda 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingDto.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class RoomRecruitingDto( +data class RoomRecruitingResponse( @SerialName("isHost") val isHost: Boolean, @SerialName("isJoining") val isJoining: Boolean, @SerialName("roomId") val roomId: Int, @@ -23,11 +23,11 @@ data class RoomRecruitingDto( @SerialName("bookTitle") val bookTitle: String, @SerialName("authorName") val authorName: String, @SerialName("bookDescription") val bookDescription: String, - @SerialName("recommandRooms") val recommendRooms: List + @SerialName("recommendRooms") val recommendRooms: List ) @Serializable -data class RecommendRoomDto( +data class RecommendRoomResponse( @SerialName("roomImageUrl") val roomImageUrl: String?, @SerialName("roomName") val roomName: String, @SerialName("memberCount") val memberCount: Int, diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/BookRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/BookRepository.kt new file mode 100644 index 00000000..f8066761 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/repository/BookRepository.kt @@ -0,0 +1,26 @@ +package com.texthip.thip.data.model.repository + +import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.book.response.BookSavedResponse +import com.texthip.thip.data.model.service.BookService +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BookRepository @Inject constructor( + private val bookService: BookService +) { + + // 저장된/모임 책 조회 API 연동 + suspend fun getBooks(type: String): Result> { + return try { + bookService.getBooks(type) + .handleBaseResponse() + .mapCatching { bookListResponse -> + bookListResponse?.bookList ?: emptyList() + } + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index f5f7170a..d7d57fa5 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -5,12 +5,11 @@ import com.texthip.thip.R import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.data.model.group.response.PaginationResult -import com.texthip.thip.data.model.group.response.RoomListDto +import com.texthip.thip.data.model.group.response.RoomMainResponse import com.texthip.thip.data.model.service.GroupService import com.texthip.thip.ui.group.done.mock.MyRoomCardData import com.texthip.thip.ui.group.done.mock.MyRoomsPaginationResult import com.texthip.thip.ui.group.myroom.mock.GroupBookData -import com.texthip.thip.data.model.book.response.BookDto import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData @@ -23,7 +22,7 @@ import javax.inject.Singleton @Singleton class GroupRepository @Inject constructor( private val groupService: GroupService, - @ApplicationContext private val context: Context + @param:ApplicationContext private val context: Context ) { private val genres = listOf( context.getString(R.string.literature), @@ -153,7 +152,7 @@ class GroupRepository @Inject constructor( } } - private fun convertToGroupCardItemRoomData(dto: RoomListDto, daysLeft: Int): GroupCardItemRoomData { + private fun convertToGroupCardItemRoomData(dto: RoomMainResponse, daysLeft: Int): GroupCardItemRoomData { return GroupCardItemRoomData( id = dto.roomId, title = dto.roomName, @@ -256,19 +255,6 @@ class GroupRepository @Inject constructor( } } - // 책 검색 API 연동 - suspend fun getBooks(type: String): Result> { - return try { - groupService.getBooks(type) - .handleBaseResponse() - .mapCatching { bookListResponse -> - bookListResponse?.bookList ?: emptyList() - } - } catch (e: Exception) { - Result.failure(e) - } - } - // 모임방 생성 API 연동 suspend fun createRoom(request: CreateRoomRequest): Result { return try { diff --git a/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt b/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt new file mode 100644 index 00000000..7e38e607 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt @@ -0,0 +1,14 @@ +package com.texthip.thip.data.model.service + +import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.book.response.BookListResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface BookService { + + @GET("books") + suspend fun getBooks( + @Query("type") type: String // "saved" 또는 "joining" + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index 35f5b2bb..fab7272f 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -4,10 +4,10 @@ import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.book.response.BookListResponse import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.data.model.group.response.CreateRoomResponse -import com.texthip.thip.data.model.group.response.JoinedRoomsDto -import com.texthip.thip.data.model.group.response.MyRoomsDto -import com.texthip.thip.data.model.group.response.RoomRecruitingDto -import com.texthip.thip.data.model.group.response.RoomsHomeDto +import com.texthip.thip.data.model.group.response.JoinedRoomListResponse +import com.texthip.thip.data.model.group.response.MyRoomListResponse +import com.texthip.thip.data.model.group.response.RoomRecruitingResponse +import com.texthip.thip.data.model.group.response.RoomMainList import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -19,21 +19,21 @@ interface GroupService { @GET("rooms/home/joined") suspend fun getJoinedRooms( @Query("page") page: Int = 1 - ): BaseResponse + ): BaseResponse @GET("rooms") suspend fun getRooms( @Query("category") category: String = "문학" // 디폴트=문학 - ): BaseResponse + ): BaseResponse @GET("rooms/my") suspend fun getMyRooms( @Query("type") type: String? = null, // "playing", "recruiting", "expired", null @Query("cursor") cursor: String? = null - ): BaseResponse + ): BaseResponse @GET("rooms/{roomId}/recruiting") - suspend fun getRoomRecruiting(@Path("roomId") roomId: Int): BaseResponse + suspend fun getRoomRecruiting(@Path("roomId") roomId: Int): BaseResponse @GET("books") suspend fun getBooks( diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt index 4ffad0bf..4367ed9a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt @@ -1,7 +1,5 @@ package com.texthip.thip.ui.group.makeroom.mock -import com.texthip.thip.R - data class BookData( val title: String, val imageUrl: String? = null, // 이미지 URL diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index 0b09c069..bba8088a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -2,9 +2,10 @@ package com.texthip.thip.ui.group.makeroom.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.book.response.BookSavedResponse import com.texthip.thip.data.model.repository.GroupRepository -import com.texthip.thip.data.model.book.response.BookDto import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.model.repository.BookRepository import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState import dagger.hilt.android.lifecycle.HiltViewModel @@ -18,7 +19,8 @@ import javax.inject.Inject @HiltViewModel class GroupMakeRoomViewModel @Inject constructor( - private val groupRepository: GroupRepository + private val groupRepository: GroupRepository, + private val bookRepository: BookRepository ) : ViewModel() { private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) @@ -55,7 +57,7 @@ class GroupMakeRoomViewModel @Inject constructor( _isLoadingBooks.value = true try { // 저장한 책 로드 - val savedBooksResult = groupRepository.getBooks("saved") + val savedBooksResult = bookRepository.getBooks("saved") savedBooksResult.onSuccess { bookDtos -> _savedBooks.value = bookDtos.map { it.toBookData() } }.onFailure { @@ -63,7 +65,7 @@ class GroupMakeRoomViewModel @Inject constructor( } // 모임 책 로드 - val groupBooksResult = groupRepository.getBooks("joining") + val groupBooksResult = bookRepository.getBooks("joining") groupBooksResult.onSuccess { bookDtos -> _groupBooks.value = bookDtos.map { it.toBookData() } }.onFailure { @@ -78,8 +80,8 @@ class GroupMakeRoomViewModel @Inject constructor( } } - // BookDto를 BookData로 변환 - private fun BookDto.toBookData(): BookData { + // BookData로 변환 + private fun BookSavedResponse.toBookData(): BookData { return BookData( title = this.bookTitle, imageUrl = this.imageUrl, From 0a9667cb9f3634bc2520a88c52fdf1dfd8c22bad Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Wed, 6 Aug 2025 18:44:38 +0900 Subject: [PATCH 33/68] =?UTF-8?q?[feat]:=20=ED=8E=98=EC=9D=B4=EC=A7=95=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../done/viewmodel/GroupDoneViewModel.kt | 43 +++---- .../ui/group/myroom/component/GroupPager.kt | 8 +- .../thip/ui/group/viewmodel/GroupViewModel.kt | 106 +++++++++++------- 3 files changed, 91 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index bd29c228..8077c6a1 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -52,28 +52,31 @@ class GroupDoneViewModel @Inject constructor( if (isLastPage && !reset) return viewModelScope.launch { - if (reset) { - _isLoading.value = true - nextCursor = null - isLastPage = false - _expiredRooms.value = emptyList() - } else { - isLoadingMore = true - } - - repository.getMyRoomsByType("expired", nextCursor) - .onSuccess { paginationResult -> - val currentList = if (reset) emptyList() else _expiredRooms.value - _expiredRooms.value = currentList + paginationResult.data - nextCursor = paginationResult.nextCursor - isLastPage = paginationResult.isLast - } - .onFailure { - // 에러 처리 (필요시 에러 상태 추가) + try { + if (reset) { + _isLoading.value = true + nextCursor = null + isLastPage = false + _expiredRooms.value = emptyList() + } else { + isLoadingMore = true } - _isLoading.value = false - isLoadingMore = false + repository.getMyRoomsByType("expired", nextCursor) + .onSuccess { paginationResult -> + val currentList = if (reset) emptyList() else _expiredRooms.value + _expiredRooms.value = currentList + paginationResult.data + nextCursor = paginationResult.nextCursor + isLastPage = paginationResult.isLast + } + .onFailure { + // 에러 발생 처리 + } + } finally { + // 성공/실패 관계없이 로딩 상태는 항상 해제 + _isLoading.value = false + isLoadingMore = false + } } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt index a6d26f9c..01e2164d 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt @@ -71,9 +71,11 @@ fun GroupPager( pageCount = { infinitePageCount } ) - // 시작 페이지로 이동 - LaunchedEffect(groupCards.size) { - pagerState.scrollToPage(startPage) + // 초기 로딩 시에만 시작 페이지로 이동 + LaunchedEffect(Unit) { + if (pagerState.currentPage == 0) { + pagerState.scrollToPage(startPage) + } } // 현재 보이는 카드 인덱스를 ViewModel에 알림 diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 1e0cba1f..6c4f2816 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -30,10 +30,14 @@ class GroupViewModel @Inject constructor( private val _isRefreshing = MutableStateFlow(false) val isRefreshing: StateFlow = _isRefreshing.asStateFlow() + private val _isLoadingMoreMyGroups = MutableStateFlow(false) + val isLoadingMoreMyGroups: StateFlow = _isLoadingMoreMyGroups.asStateFlow() + private var currentMyGroupsPage = 1 private var loadedPagesCount = 0 - private val PAGES_PER_BATCH = 5 // 5페이지씩 미리 로드 - private val PRELOAD_THRESHOLD = 3 // 3페이지에 도달하면 다음 배치 로드 + private val PAGES_PER_BATCH = 3 // 5페이지에서 3페이지로 줄임 + private val PRELOAD_THRESHOLD = 2 // 임계점도 2로 줄임 + private var isBatchLoading = false private val _roomSections = MutableStateFlow>(emptyList()) val roomSections: StateFlow> = _roomSections.asStateFlow() @@ -87,32 +91,45 @@ class GroupViewModel @Inject constructor( loadedPagesCount = 0 _myGroups.value = emptyList() _hasMoreMyGroups.value = true + _isRefreshing.value = true + } + try { + // Pager를 위한 초기 배치 로드 + loadPageBatch() + } finally { + _isRefreshing.value = false } - // 초기 로드 시 첫 번째 배치(5페이지) 미리 로드 - loadPageBatch() } private fun loadPageBatch() = viewModelScope.launch { - if (!_hasMoreMyGroups.value) return@launch + if (!_hasMoreMyGroups.value || isBatchLoading) return@launch - // 5페이지씩 배치로 로드 - val currentBatchStart = currentMyGroupsPage - val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 - - for (page in currentBatchStart..batchEndPage) { - if (!_hasMoreMyGroups.value) break + try { + isBatchLoading = true + _isLoadingMoreMyGroups.value = true - repository.getMyJoinedRooms(page) - .onSuccess { paginationResult -> - _myGroups.value = _myGroups.value + paginationResult.data - _hasMoreMyGroups.value = paginationResult.hasMore - loadedPagesCount++ - currentMyGroupsPage = page + 1 - } - .onFailure { - // 에러 처리 - 배치 로딩 중단 - break - } + // 3페이지씩 배치로 로드 (Pager 미리보기용) + val currentBatchStart = currentMyGroupsPage + val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 + + for (page in currentBatchStart..batchEndPage) { + if (!_hasMoreMyGroups.value) break + + repository.getMyJoinedRooms(page) + .onSuccess { paginationResult -> + _myGroups.value = _myGroups.value + paginationResult.data + _hasMoreMyGroups.value = paginationResult.hasMore + loadedPagesCount++ + currentMyGroupsPage = page + 1 + } + .onFailure { + // 에러 발생 시 배치 로딩 중단 + break + } + } + } finally { + isBatchLoading = false + _isLoadingMoreMyGroups.value = false } } @@ -121,16 +138,12 @@ class GroupViewModel @Inject constructor( val totalCards = _myGroups.value.size val currentPageEquivalent = (cardIndex / 3) + 1 // 3개씩 한 페이지라고 가정 - // 현재 로드된 페이지의 3페이지 지점에 도달하면 다음 배치 로드 + // 현재 로드된 페이지의 임계점에 도달하면 다음 배치 로드 if (currentPageEquivalent >= loadedPagesCount - PRELOAD_THRESHOLD && - _hasMoreMyGroups.value) { + _hasMoreMyGroups.value && !isBatchLoading) { loadPageBatch() } } - - fun loadMoreMyGroups() { - loadPageBatch() - } private fun loadRoomSections() { viewModelScope.launch { @@ -197,23 +210,30 @@ class GroupViewModel @Inject constructor( _myGroups.value = emptyList() _hasMoreMyGroups.value = true - // 첫 번째 배치 로드 - val currentBatchStart = currentMyGroupsPage - val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 + // 첫 번째 배치 로드 (3페이지) + if (!_hasMoreMyGroups.value || isBatchLoading) return@async - for (page in currentBatchStart..batchEndPage) { - if (!_hasMoreMyGroups.value) break + try { + isBatchLoading = true + val currentBatchStart = currentMyGroupsPage + val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 - repository.getMyJoinedRooms(page) - .onSuccess { paginationResult -> - _myGroups.value = _myGroups.value + paginationResult.data - _hasMoreMyGroups.value = paginationResult.hasMore - loadedPagesCount++ - currentMyGroupsPage = page + 1 - } - .onFailure { - break - } + for (page in currentBatchStart..batchEndPage) { + if (!_hasMoreMyGroups.value) break + + repository.getMyJoinedRooms(page) + .onSuccess { paginationResult -> + _myGroups.value = _myGroups.value + paginationResult.data + _hasMoreMyGroups.value = paginationResult.hasMore + loadedPagesCount++ + currentMyGroupsPage = page + 1 + } + .onFailure { + break + } + } + } finally { + isBatchLoading = false } }, async { From 71c1e5290d4f8ace464acd9069aaec965a27add4 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Wed, 6 Aug 2025 21:33:15 +0900 Subject: [PATCH 34/68] =?UTF-8?q?[feat]:=20=EB=82=B4=20=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=EB=B0=A9=20DTO=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/model/group/response/MyRoomListResponse.kt | 6 ++++-- .../texthip/thip/data/model/repository/GroupRepository.kt | 6 ++++-- .../com/texthip/thip/data/model/service/GroupService.kt | 2 +- .../texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt | 7 ++++--- .../com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt | 8 ++++---- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt index e36aef9c..b66ad0f6 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt @@ -14,7 +14,9 @@ data class MyRoomListResponse( data class MyRoomResponse( @SerialName("roomId") val roomId: Int, @SerialName("bookImageUrl") val bookImageUrl: String, - @SerialName("bookTitle") val bookTitle: String, + @SerialName("roomName") val roomName: String, + @SerialName("recruitCount") val recruitCount: Int, @SerialName("memberCount") val memberCount: Int, - @SerialName("endDate") val endDate: String? // "완료된" 모임방의 경우 null + @SerialName("endDate") val endDate: String, + @SerialName("type") val type: String // "playingAndRecruiting", "recruiting", "playing", "expired" ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index d7d57fa5..b7c12e60 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -130,9 +130,11 @@ class GroupRepository @Inject constructor( MyRoomCardData( roomId = room.roomId, bookImageUrl = room.bookImageUrl, - bookTitle = room.bookTitle, + roomName = room.roomName, + recruitCount = room.recruitCount, memberCount = room.memberCount, - endDate = room.endDate + endDate = room.endDate, + type = room.type ) } diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index fab7272f..8d4c3ebd 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -28,7 +28,7 @@ interface GroupService { @GET("rooms/my") suspend fun getMyRooms( - @Query("type") type: String? = null, // "playing", "recruiting", "expired", null + @Query("type") type: String? = null, // "playingAndRecruiting", "recruiting", "playing", "expired" @Query("cursor") cursor: String? = null ): BaseResponse diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt index 25a4cc6d..296d24fe 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt @@ -94,10 +94,11 @@ fun GroupDoneScreen( items(expiredRooms) { room -> CardItemRoom( - title = room.bookTitle, + title = room.roomName, + imageUrl = room.bookImageUrl, participants = room.memberCount, - maxParticipants = room.memberCount, // 완료된 모임방은 maxParticipants = memberCount - isRecruiting = false, // 완료된 모임방은 항상 false + maxParticipants = room.recruitCount, // 모집 인원 수 사용 + isRecruiting = room.type == "recruiting", onClick = { /* 완료된 모임방은 클릭 불가 */ } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt index 5825eadf..0b64e0dd 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -1,13 +1,13 @@ package com.texthip.thip.ui.group.done.mock -import com.texthip.thip.R - data class MyRoomCardData( val roomId: Int, val bookImageUrl: String?, // API에서 받은 이미지 URL - val bookTitle: String, + val roomName: String, + val recruitCount: Int, val memberCount: Int, - val endDate: String? = null + val endDate: String, + val type: String // "playingAndRecruiting", "recruiting", "playing", "expired" ) data class MyRoomsPaginationResult( From c22f24ad76dcf5c88bc58d3302efc7817b292b8a Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Wed, 6 Aug 2025 21:34:16 +0900 Subject: [PATCH 35/68] =?UTF-8?q?[feat]:=20=EB=82=B4=20=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=EB=B0=A9(=EC=A7=84=ED=96=89=EC=A4=91,=20=EB=AA=A8=EC=A7=91?= =?UTF-8?q?=EC=A4=91)=20=EC=97=B0=EA=B2=B0=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../myroom/component/GroupMyRoomFilterRow.kt | 2 + .../ui/group/myroom/screen/GroupMyScreen.kt | 309 +++++++----------- .../myroom/viewmodel/GroupMyViewModel.kt | 95 ++++++ .../navigator/navigations/GroupNavigation.kt | 13 +- 4 files changed, 221 insertions(+), 198 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMyRoomFilterRow.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMyRoomFilterRow.kt index ab7067fe..33166c67 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMyRoomFilterRow.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMyRoomFilterRow.kt @@ -25,11 +25,13 @@ fun GroupMyRoomFilterRow( OptionChipButton( text = stringResource(R.string.on_going), isFilled = true, + isSelected = selectedStates[0], onClick = { onToggle(0) } ) OptionChipButton( text = stringResource(R.string.recruiting), isFilled = true, + isSelected = selectedStates[1], onClick = { onToggle(1) } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index efe3ce22..e41d109e 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -10,43 +10,63 @@ 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.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox 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.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.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.group.done.mock.MyRoomCardData import com.texthip.thip.ui.group.myroom.component.GroupMyRoomFilterRow -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.theme.ThipTheme +import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GroupMyScreen( - allDataList: List, - onCardClick: (GroupCardItemRoomData) -> Unit = {}, - onNavigateBack: () -> Unit = {} + onCardClick: (MyRoomCardData) -> Unit = {}, + onNavigateBack: () -> Unit = {}, + viewModel: GroupMyViewModel = hiltViewModel() ) { - var selectedStates by remember { mutableStateOf(booleanArrayOf(false, false)) } - - val filteredList = remember(selectedStates, allDataList) { - if (selectedStates.all { !it } || selectedStates.all { it }) { - allDataList - } else if (selectedStates[0]) { - allDataList.filter { !it.isRecruiting } - } else if (selectedStates[1]) { - allDataList.filter { it.isRecruiting } - } else { - allDataList + val myRooms by viewModel.myRooms.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + val currentRoomType by viewModel.currentRoomType.collectAsState() + val listState = rememberLazyListState() + + // 무한 스크롤 로직 + val shouldLoadMore by remember { + derivedStateOf { + val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + val totalItems = listState.layoutInfo.totalItemsCount + lastVisibleIndex >= totalItems - 3 + } + } + + LaunchedEffect(shouldLoadMore) { + if (shouldLoadMore && myRooms.isNotEmpty()) { + viewModel.loadMoreMyRooms() + } + } + + // Filter 상태를 room type에 따라 설정 (토글 방식) + val selectedStates = remember(currentRoomType) { + when (currentRoomType) { + GroupMyViewModel.PLAYING -> booleanArrayOf(true, false) + GroupMyViewModel.RECRUITING -> booleanArrayOf(false, true) + else -> booleanArrayOf(false, false) // playingAndRecruiting (둘 다 비활성화 = 전체) } } @@ -59,188 +79,93 @@ fun GroupMyScreen( title = stringResource(R.string.my_group_room), onLeftClick = onNavigateBack, ) - Column( - Modifier - .background(colors.Black) - .fillMaxSize() - .padding(horizontal = 20.dp) + + PullToRefreshBox( + isRefreshing = isLoading, + onRefresh = { viewModel.refreshData() }, + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.height(20.dp)) + Column( + Modifier + .background(colors.Black) + .fillMaxSize() + .padding(horizontal = 20.dp) + ) { + Spacer(modifier = Modifier.height(20.dp)) - GroupMyRoomFilterRow( - selectedStates = selectedStates, - onToggle = { idx -> - selectedStates = selectedStates.copyOf().also { it[idx] = !it[idx] } - } - ) + GroupMyRoomFilterRow( + selectedStates = selectedStates, + onToggle = { idx -> + val newRoomType = when { + // 진행중 버튼을 눌렀을 때 + idx == 0 -> { + if (selectedStates[0]) { + // 이미 선택된 상태면 전체로 변경 + GroupMyViewModel.PLAYING_AND_RECRUITING + } else { + // 선택되지 않은 상태면 진행중만 + GroupMyViewModel.PLAYING + } + } + // 모집중 버튼을 눌렀을 때 + idx == 1 -> { + if (selectedStates[1]) { + // 이미 선택된 상태면 전체로 변경 + GroupMyViewModel.PLAYING_AND_RECRUITING + } else { + // 선택되지 않은 상태면 모집중만 + GroupMyViewModel.RECRUITING + } + } + else -> GroupMyViewModel.PLAYING_AND_RECRUITING + } + viewModel.changeRoomType(newRoomType) + } + ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(20.dp)) - if (filteredList.isNotEmpty()) { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(20.dp), - contentPadding = PaddingValues(bottom = 20.dp), - modifier = Modifier.fillMaxSize() - ) { - items(filteredList) { item -> - CardItemRoom( - title = item.title, - participants = item.participants, - maxParticipants = item.maxParticipants, - isRecruiting = item.isRecruiting, - endDate = item.endDate, - imageUrl = item.imageUrl, - onClick = { onCardClick(item) } - ) + if (myRooms.isNotEmpty()) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues(bottom = 20.dp), + modifier = Modifier.fillMaxSize() + ) { + items(myRooms) { room -> + CardItemRoom( + title = room.roomName, + participants = room.memberCount, + maxParticipants = room.recruitCount, + isRecruiting = room.type == "recruiting" || room.type == "playingAndRecruiting", + imageUrl = room.bookImageUrl, + onClick = { onCardClick(room) } + ) + } } - } - } else { - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.group_myroom_error_comment1), - color = colors.White, - style = typography.smalltitle_sb600_s18_h24 - ) - Spacer(modifier = Modifier.height(8.dp)) + } else if (!isLoading) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.group_myroom_error_comment1), + color = colors.White, + style = typography.smalltitle_sb600_s18_h24 + ) + Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(R.string.group_myroom_error_comment2), - color = colors.Grey, - style = typography.copy_r400_s14 - ) + Text( + text = stringResource(R.string.group_myroom_error_comment2), + color = colors.Grey, + style = typography.copy_r400_s14 + ) + } } } } } } -@Preview -@Composable -fun MyGroupListFilterScreenPreview() { - ThipTheme { - val dataList = listOf( - GroupCardItemRoomData( - id = 1, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 2, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 30, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 3, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 1, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 4, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 5, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 6, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 30, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 7, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 1, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 8, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 9, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 10, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 30, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 11, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 1, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 12, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 3, - genreIndex = 0 - ) - ) - GroupMyScreen(allDataList = dataList) - } -} - -@Preview() -@Composable -fun MyGroupListEmptyScreenPreview() { - ThipTheme { - GroupMyScreen(allDataList = emptyList()) - } -} diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt new file mode 100644 index 00000000..b227f63a --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -0,0 +1,95 @@ +package com.texthip.thip.ui.group.myroom.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GroupMyViewModel @Inject constructor( + private val repository: GroupRepository +) : ViewModel() { + + private val _myRooms = MutableStateFlow>(emptyList()) + val myRooms: StateFlow> = _myRooms.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _isLoadingMore = MutableStateFlow(false) + val isLoadingMore: StateFlow = _isLoadingMore.asStateFlow() + + private val _currentRoomType = MutableStateFlow("playingAndRecruiting") + val currentRoomType: StateFlow = _currentRoomType.asStateFlow() + + private var nextCursor: String? = null + private var isLastPage = false + private var isLoadingData = false + + init { + loadMyRooms(reset = true) + } + + fun loadMyRooms(reset: Boolean = false) { + if (isLoadingData && !reset) return + if (isLastPage && !reset) return + + viewModelScope.launch { + try { + isLoadingData = true + + if (reset) { + _isLoading.value = true + nextCursor = null + isLastPage = false + _myRooms.value = emptyList() + } else { + _isLoadingMore.value = true + } + + repository.getMyRoomsByType(_currentRoomType.value, nextCursor) + .onSuccess { paginationResult -> + val currentList = if (reset) emptyList() else _myRooms.value + _myRooms.value = currentList + paginationResult.data + nextCursor = paginationResult.nextCursor + isLastPage = paginationResult.isLast + } + .onFailure { + // 에러 처리 (필요시 에러 상태 추가) + } + } finally { + isLoadingData = false + _isLoading.value = false + _isLoadingMore.value = false + } + } + } + + fun loadMoreMyRooms() { + loadMyRooms(reset = false) + } + + fun refreshData() { + loadMyRooms(reset = true) + } + + fun changeRoomType(roomType: String) { + if (roomType != _currentRoomType.value) { + _currentRoomType.value = roomType + loadMyRooms(reset = true) + } + } + + // Room type + companion object { + const val PLAYING_AND_RECRUITING = "playingAndRecruiting" + const val RECRUITING = "recruiting" + const val PLAYING = "playing" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index f0d95111..f0aab292 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -17,6 +17,7 @@ import com.texthip.thip.ui.group.room.screen.GroupRoomScreen import com.texthip.thip.ui.group.screen.GroupScreen import com.texthip.thip.ui.group.search.screen.GroupSearchScreen import com.texthip.thip.ui.group.viewmodel.GroupViewModel +import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel import com.texthip.thip.ui.navigator.extensions.navigateToAlarm import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom @@ -89,16 +90,16 @@ fun NavGraphBuilder.groupNavigation( // Group My 화면 composable { - val groupViewModel: GroupViewModel = hiltViewModel() - val myRoomGroups by groupViewModel.myRoomGroups.collectAsState() + val groupMyViewModel: GroupMyViewModel = hiltViewModel() GroupMyScreen( - allDataList = myRoomGroups, + viewModel = groupMyViewModel, onCardClick = { room -> - if (room.isRecruiting) { - navController.navigateToGroupRecruit(room.id) + val isRecruiting = room.type == "recruiting" + if (isRecruiting) { + navController.navigateToGroupRecruit(room.roomId) } else { - navController.navigateToGroupRoom(room.id) + navController.navigateToGroupRoom(room.roomId) } }, onNavigateBack = { From 4dcc3616117e82a33af83ac0a46d23fde031aa33 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Wed, 6 Aug 2025 22:05:37 +0900 Subject: [PATCH 36/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=A7=91=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20DTO=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/group/response/RoomRecruitingResponse.kt | 2 ++ .../thip/data/model/repository/GroupRepository.kt | 11 +++++------ .../texthip/thip/data/model/service/GroupService.kt | 5 ----- .../ui/group/myroom/mock/GroupCardItemRoomData.kt | 2 -- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt index 81fc9bda..0d35628a 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt @@ -23,11 +23,13 @@ data class RoomRecruitingResponse( @SerialName("bookTitle") val bookTitle: String, @SerialName("authorName") val authorName: String, @SerialName("bookDescription") val bookDescription: String, + @SerialName("publisher") val publisher: String, @SerialName("recommendRooms") val recommendRooms: List ) @Serializable data class RecommendRoomResponse( + @SerialName("roomId") val roomId: Int, @SerialName("roomImageUrl") val roomImageUrl: String?, @SerialName("roomName") val roomName: String, @SerialName("memberCount") val memberCount: Int, diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index b7c12e60..32077d11 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -162,7 +162,7 @@ class GroupRepository @Inject constructor( maxParticipants = dto.recruitCount, isRecruiting = true, endDate = daysLeft, - imageUrl = dto.bookImageUrl, // API에서 받은 실제 이미지 URL + imageUrl = dto.bookImageUrl, genreIndex = 0, isSecret = false ) @@ -203,23 +203,22 @@ class GroupRepository @Inject constructor( val bookData = GroupBookData( title = data.bookTitle, author = data.authorName, - publisher = "출판사 정보 없음", // API에서 제공하지 않음 + publisher = data.publisher, description = data.bookDescription, - imageUrl = data.bookImageUrl // API에서 받은 실제 이미지 URL + imageUrl = data.bookImageUrl ) // 추천 모임방 변환 val recommendations = data.recommendRooms.map { recommendDto -> GroupCardItemRoomData( - id = recommendDto.hashCode(), // 임시 ID (실제로는 roomId가 필요) + id = recommendDto.roomId, // API에서 제공하는 실제 roomId title = recommendDto.roomName, participants = recommendDto.memberCount, maxParticipants = recommendDto.recruitCount, isRecruiting = true, endDate = extractDaysFromDeadline(recommendDto.recruitEndDate), - imageUrl = recommendDto.roomImageUrl, // API에서 받은 실제 이미지 URL + imageUrl = recommendDto.roomImageUrl, genreIndex = 0, // 기본값 - isSecret = true // 기본값 ) } diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index 8d4c3ebd..2b0c51cd 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -35,11 +35,6 @@ interface GroupService { @GET("rooms/{roomId}/recruiting") suspend fun getRoomRecruiting(@Path("roomId") roomId: Int): BaseResponse - @GET("books") - suspend fun getBooks( - @Query("type") type: String // "saved" 또는 "joining" - ): BaseResponse - @POST("rooms") suspend fun createRoom( @Body request: CreateRoomRequest diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt index 4a76eed9..5ea7fa00 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt @@ -1,7 +1,5 @@ package com.texthip.thip.ui.group.myroom.mock -import com.texthip.thip.R - data class GroupCardItemRoomData( val id: Int, val title: String, From 5ba236405110b8b8a18a7ee06d32996a211f1356 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Wed, 6 Aug 2025 22:06:00 +0900 Subject: [PATCH 37/68] =?UTF-8?q?[feat]:=20=EB=82=B4=20=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=EB=B0=A9(=EC=A7=84=ED=96=89=EC=A4=91,=20=EB=AA=A8=EC=A7=91?= =?UTF-8?q?=EC=A4=91)=20util=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80=20(#65?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/done/Screen/GroupDoneScreen.kt | 3 ++- .../thip/ui/group/done/mock/MyRoomCardData.kt | 23 ++++++++++++++++++- .../ui/group/myroom/screen/GroupMyScreen.kt | 5 +++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt index 296d24fe..e6eb55cf 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt @@ -26,6 +26,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.group.done.mock.isRecruitingByType import com.texthip.thip.ui.group.done.viewmodel.GroupDoneViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -98,7 +99,7 @@ fun GroupDoneScreen( imageUrl = room.bookImageUrl, participants = room.memberCount, maxParticipants = room.recruitCount, // 모집 인원 수 사용 - isRecruiting = room.type == "recruiting", + isRecruiting = room.isRecruitingByType(), onClick = { /* 완료된 모임방은 클릭 불가 */ } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt index 0b64e0dd..1fc951bd 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -14,4 +14,25 @@ data class MyRoomsPaginationResult( val data: List, val nextCursor: String?, val isLast: Boolean -) \ No newline at end of file +) + +// MyRoomCardData를 위한 타입 기반 모집 상태 확인 함수 +fun MyRoomCardData.isRecruitingByType(): Boolean { + return when (type) { + "recruiting" -> true + "playingAndRecruiting" -> false + "playing" -> false + "expired" -> false + else -> false // 타입 정보가 없으면 기본값 + } +} + +// endDate 문자열을 일수로 변환하는 함수 (예: "25일 뒤" -> 25) +fun MyRoomCardData.getEndDateInDays(): Int { + return when { + endDate.contains("일 뒤") -> { + endDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 + } + else -> 0 // 파싱할 수 없는 경우 0 반환 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index e41d109e..3b15838f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -29,6 +29,8 @@ import com.texthip.thip.R import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import com.texthip.thip.ui.group.done.mock.isRecruitingByType +import com.texthip.thip.ui.group.done.mock.getEndDateInDays import com.texthip.thip.ui.group.myroom.component.GroupMyRoomFilterRow import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel import com.texthip.thip.ui.theme.ThipTheme.colors @@ -137,7 +139,8 @@ fun GroupMyScreen( title = room.roomName, participants = room.memberCount, maxParticipants = room.recruitCount, - isRecruiting = room.type == "recruiting" || room.type == "playingAndRecruiting", + isRecruiting = room.isRecruitingByType(), + endDate = room.getEndDateInDays(), imageUrl = room.bookImageUrl, onClick = { onCardClick(room) } ) From b50375e8cd873e56ee4b9b77c425786e6b00af14 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 10:18:03 +0900 Subject: [PATCH 38/68] =?UTF-8?q?[feat]:=20GroupRoomRecruitViewModel=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../room/screen/GroupRoomRecruitScreen.kt | 321 ++++-------------- .../viewmodel/GroupRoomRecruitViewModel.kt | 127 +++++++ 2 files changed, 187 insertions(+), 261 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index b3c326c0..10942ebf 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -22,10 +22,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +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.graphics.Brush @@ -47,7 +45,7 @@ import com.texthip.thip.ui.group.myroom.mock.GroupBookData import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomData -import com.texthip.thip.ui.group.viewmodel.GroupViewModel +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -56,41 +54,37 @@ import kotlinx.coroutines.delay @Composable fun GroupRoomRecruitScreen( roomId: Int, - viewModel: GroupViewModel? = hiltViewModel(), + viewModel: GroupRoomRecruitViewModel = hiltViewModel(), mockRoomDetail: GroupRoomData? = null, // Preview용 mock 데이터 onRecommendationClick: (GroupCardItemRoomData) -> Unit = {}, - onParticipation: () -> Unit = {}, // 참여 - onCancelParticipation: () -> Unit = {}, // 참여 취소 - onCloseRecruitment: () -> Unit = {}, // 모집 마감 - onBackClick: () -> Unit = {} // 뒤로가기 추가 + onNavigateToGroupScreen: (String) -> Unit = {}, // GroupScreen으로 네비게이션 + 토스트 메시지 + onBackClick: () -> Unit = {} // 뒤로가기 ) { val context = LocalContext.current - var roomDetail by remember { mutableStateOf(mockRoomDetail) } - var isLoading by remember { mutableStateOf(mockRoomDetail == null && viewModel != null) } + // ViewModel states + val roomDetail by viewModel.roomDetail.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + val currentButtonType by viewModel.currentButtonType.collectAsState() + val showToast by viewModel.showToast.collectAsState() + val toastMessage by viewModel.toastMessage.collectAsState() + val showDialog by viewModel.showDialog.collectAsState() + val dialogTitle by viewModel.dialogTitle.collectAsState() + val dialogDescription by viewModel.dialogDescription.collectAsState() + val shouldNavigateToGroupScreen by viewModel.shouldNavigateToGroupScreen.collectAsState() - var currentButtonType by remember { mutableStateOf(mockRoomDetail?.buttonType) } - var showToast by remember { mutableStateOf(false) } - var toastMessage by remember { mutableStateOf("") } - var showDialog by remember { mutableStateOf(false) } - var dialogTitle by remember { mutableStateOf("") } - var dialogDescription by remember { mutableStateOf("") } - var pendingAction by remember { mutableStateOf<(() -> Unit)?>(null) } + // 데이터 로딩 - mockRoomDetail이 없을 때만 실행 + LaunchedEffect(roomId) { + if (mockRoomDetail == null) { + viewModel.loadRoomDetail(roomId) + } + } - // 데이터 로딩 - ViewModel이 있고 mockRoomDetail이 없을 때만 실행 - LaunchedEffect(roomId, viewModel) { - if (viewModel != null && mockRoomDetail == null) { - isLoading = true - - viewModel.getRoomRecruiting(roomId) - .onSuccess { data -> - roomDetail = data - currentButtonType = data.buttonType - isLoading = false - } - .onFailure { error -> - isLoading = false - } + // GroupScreen으로 네비게이션 처리 + LaunchedEffect(shouldNavigateToGroupScreen) { + if (shouldNavigateToGroupScreen) { + onNavigateToGroupScreen(toastMessage) // 토스트 메시지와 함께 네비게이션 + viewModel.onNavigatedToGroupScreen() } } @@ -106,8 +100,9 @@ fun GroupRoomRecruitScreen( return@Box } - // 데이터가 없는 경우 - val detail = roomDetail ?: return@Box + // 데이터가 없는 경우 (Preview에서는 mock 데이터 사용) + val detail = roomDetail ?: mockRoomDetail ?: return@Box + Image( painter = painterResource(id = R.drawable.group_room_recruiting), contentDescription = "배경 이미지", @@ -145,7 +140,7 @@ fun GroupRoomRecruitScreen( DefaultTopAppBar( isRightIconVisible = false, isTitleVisible = false, - onLeftClick = onBackClick, // 뒤로가기 콜백 연결 + onLeftClick = onBackClick, ) Column( @@ -364,51 +359,35 @@ fun GroupRoomRecruitScreen( } } - // 하단 버튼 - if (currentButtonType != null) { - val buttonText = when (currentButtonType) { + // 하단 버튼 (Preview에서는 mockRoomDetail의 buttonType 사용) + val buttonType = currentButtonType ?: mockRoomDetail?.buttonType + if (buttonType != null) { + val buttonText = when (buttonType) { GroupBottomButtonType.JOIN -> stringResource(R.string.group_room_screen_participant) GroupBottomButtonType.CANCEL -> stringResource(R.string.group_room_screen_cancel) GroupBottomButtonType.CLOSE -> stringResource(R.string.group_room_screen_end) - null -> TODO() } Button( onClick = { - when (currentButtonType) { + when (buttonType) { GroupBottomButtonType.JOIN -> { - onParticipation() // 외부 콜백 호출 - showToast = true - toastMessage = context.getString(R.string.group_participant_complete_alarm) - currentButtonType = GroupBottomButtonType.CANCEL + viewModel.onParticipationClick() } GroupBottomButtonType.CANCEL -> { - dialogTitle = context.getString(R.string.group_participant_cancel_popup) - dialogDescription = - context.getString(R.string.group_participant_cancel_comment) - pendingAction = { - onCancelParticipation() - showToast = true - toastMessage = - context.getString(R.string.group_participant_cancel_alarm) - currentButtonType = GroupBottomButtonType.JOIN - } - showDialog = true + viewModel.onCancelParticipationClick( + dialogTitle = context.getString(R.string.group_participant_cancel_popup), + dialogDescription = context.getString(R.string.group_participant_cancel_comment) + ) } GroupBottomButtonType.CLOSE -> { - dialogTitle = context.getString(R.string.group_participant_close_popup) - dialogDescription = - context.getString(R.string.group_participant_close_comment) - pendingAction = { - onCloseRecruitment() - showToast = true - toastMessage = context.getString(R.string.group_participant_close_alarm) - } - showDialog = true + viewModel.onCloseRecruitmentClick( + dialogTitle = context.getString(R.string.group_participant_close_popup), + dialogDescription = context.getString(R.string.group_participant_close_comment) + ) } - null -> {} // currentButtonType이 null인 경우 아무것도 하지 않음 } }, colors = ButtonDefaults.buttonColors( @@ -428,8 +407,8 @@ fun GroupRoomRecruitScreen( } } - // 토스트 팝업 - if (showToast) { + // 토스트 팝업 - GroupScreen으로 네비게이션할 때는 표시하지 않음 + if (showToast && !shouldNavigateToGroupScreen) { ToastWithDate( message = toastMessage, modifier = Modifier @@ -451,76 +430,38 @@ fun GroupRoomRecruitScreen( title = dialogTitle, description = dialogDescription, onConfirm = { - showDialog = false - pendingAction?.invoke() + viewModel.onDialogConfirm() }, onCancel = { - showDialog = false - pendingAction = null + viewModel.onDialogCancel() } ) } } } - // 토스트 3초 - LaunchedEffect(showToast) { - if (showToast) { + // 토스트 3초 후 자동 숨김 (GroupScreen으로 네비게이션 시에는 GroupScreen에서 관리) + LaunchedEffect(showToast, shouldNavigateToGroupScreen) { + if (showToast && !shouldNavigateToGroupScreen) { delay(3000) - showToast = false + viewModel.hideToast() } } } @Preview(name = "참여 버튼 상태") @Composable -fun GroupRoomRecruitScreenPreviewJoin() { +fun GroupRoomRecruitScreenPreview() { ThipTheme { val recommendations = listOf( GroupCardItemRoomData( id = 1, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", + title = "일본 소설 좋아하는 사람들", participants = 19, maxParticipants = 25, isRecruiting = true, endDate = 2, genreIndex = 0 - ), - GroupCardItemRoomData( - id = 2, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 12, - maxParticipants = 16, - isRecruiting = true, - endDate = 6, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 3, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 30, - maxParticipants = 30, - isRecruiting = false, - endDate = 0, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 4, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 10, - maxParticipants = 12, - isRecruiting = true, - endDate = 8, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 5, - title = "에세이 나눔방", - participants = 14, - maxParticipants = 20, - isRecruiting = true, - endDate = 4, - genreIndex = 0 ) ) @@ -528,7 +469,7 @@ fun GroupRoomRecruitScreenPreviewJoin() { title = "심장보다 단단한 토마토 한 알", author = "고선지", publisher = "푸른출판사", - description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", + description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집.", imageUrl = null ) @@ -536,7 +477,7 @@ fun GroupRoomRecruitScreenPreviewJoin() { id = 1, title = "시집만 읽는 사람들 3월", isSecret = true, - description = "'시집만 읽는 사람들' 3월 모임입니다. 이번 달 모임에서는 심장보다 단단한 토마토 한 알을 함께 읽어요.", + description = "'시집만 읽는 사람들' 3월 모임입니다.", startDate = "2025.01.12", endDate = "2025.02.12", members = 22, @@ -544,157 +485,15 @@ fun GroupRoomRecruitScreenPreviewJoin() { daysLeft = 4, genre = "문학", bookData = bookData, - recommendations = recommendations + recommendations = recommendations, + buttonType = GroupBottomButtonType.JOIN ) GroupRoomRecruitScreen( roomId = 1, - viewModel = null, - mockRoomDetail = detailJoin.copy(buttonType = GroupBottomButtonType.JOIN), - onRecommendationClick = {}, - onParticipation = {}, - onCancelParticipation = {}, - onCloseRecruitment = {}, - onBackClick = {} - ) - } -} - -@Preview(name = "참여 취소 버튼 상태") -@Composable -fun GroupRoomRecruitScreenPreviewCancel() { - ThipTheme { - val recommendations = listOf( - GroupCardItemRoomData( - id = 6, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 19, - maxParticipants = 25, - isRecruiting = true, - endDate = 2, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 7, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 12, - maxParticipants = 16, - isRecruiting = true, - endDate = 6, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 8, - title = "에세이 나눔방", - participants = 14, - maxParticipants = 20, - isRecruiting = true, - endDate = 4, - genreIndex = 0 - ) - ) - - val bookData = GroupBookData( - title = "심장보다 단단한 토마토 한 알", - author = "고선지", - publisher = "푸른출판사", - description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", - imageUrl = null - ) - - val detailCancel = GroupRoomData( - id = 2, - title = "시집만 읽는 사람들 3월", - isSecret = true, - description = "'시집만 읽는 사람들' 3월 모임입니다. 이번 달 모임에서는 심장보다 단단한 토마토 한 알을 함께 읽어요.", - startDate = "2025.01.12", - endDate = "2025.02.12", - members = 23, // 참여 후 인원 증가 - maxMembers = 30, - daysLeft = 4, - genre = "고전 문학", - bookData = bookData, - recommendations = recommendations - ) - - GroupRoomRecruitScreen( - roomId = 2, - viewModel = null, - mockRoomDetail = detailCancel.copy(buttonType = GroupBottomButtonType.CANCEL), - onRecommendationClick = {}, - onParticipation = {}, - onCancelParticipation = {}, - onCloseRecruitment = {}, - onBackClick = {} - ) - } -} - -@Preview(name = "모집 마감 버튼 상태") -@Composable -fun GroupRoomRecruitScreenClose() { - ThipTheme { - val recommendations = listOf( - GroupCardItemRoomData( - id = 9, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 19, - maxParticipants = 25, - isRecruiting = true, - endDate = 2, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 10, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 12, - maxParticipants = 16, - isRecruiting = true, - endDate = 6, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 11, - title = "미스터리 소설 탐구", - participants = 8, - maxParticipants = 15, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ) - ) - - val bookData = GroupBookData( - title = "심장보다 단단한 토마토 한 알", - author = "고선지", - publisher = "푸른출판사", - description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", - imageUrl = null - ) - - val detailClose = GroupRoomData( - id = 3, - title = "시집만 읽는 사람들 3월", - isSecret = false, // 오픈방으로 변경 - description = "'시집만 읽는 사람들' 3월 모임입니다. 이번 달 모임에서는 심장보다 단단한 토마토 한 알을 함께 읽어요. 모임장이 모집을 마감할 수 있는 상태입니다.", - startDate = "2025.01.12", - endDate = "2025.02.12", - members = 15, // 적절한 인원 - maxMembers = 30, - daysLeft = 7, // 마감일이 조금 더 남음 - genre = "문학", - bookData = bookData, - recommendations = recommendations - ) - - GroupRoomRecruitScreen( - roomId = 3, - viewModel = null, - mockRoomDetail = detailClose.copy(buttonType = GroupBottomButtonType.CLOSE), + mockRoomDetail = detailJoin, onRecommendationClick = {}, - onParticipation = {}, - onCancelParticipation = {}, - onCloseRecruitment = {}, + onNavigateToGroupScreen = {}, onBackClick = {} ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt new file mode 100644 index 00000000..1b94d597 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -0,0 +1,127 @@ +package com.texthip.thip.ui.group.room.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType +import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GroupRoomRecruitViewModel @Inject constructor( + private val repository: GroupRepository +) : ViewModel() { + + private val _roomDetail = MutableStateFlow(null) + val roomDetail: StateFlow = _roomDetail.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _currentButtonType = MutableStateFlow(null) + val currentButtonType: StateFlow = _currentButtonType.asStateFlow() + + private val _showToast = MutableStateFlow(false) + val showToast: StateFlow = _showToast.asStateFlow() + + private val _toastMessage = MutableStateFlow("") + val toastMessage: StateFlow = _toastMessage.asStateFlow() + + private val _showDialog = MutableStateFlow(false) + val showDialog: StateFlow = _showDialog.asStateFlow() + + private val _dialogTitle = MutableStateFlow("") + val dialogTitle: StateFlow = _dialogTitle.asStateFlow() + + private val _dialogDescription = MutableStateFlow("") + val dialogDescription: StateFlow = _dialogDescription.asStateFlow() + + private val _shouldNavigateToGroupScreen = MutableStateFlow(false) + val shouldNavigateToGroupScreen: StateFlow = _shouldNavigateToGroupScreen.asStateFlow() + + private var pendingAction: (() -> Unit)? = null + + fun loadRoomDetail(roomId: Int) { + viewModelScope.launch { + _isLoading.value = true + + repository.getRoomRecruiting(roomId) + .onSuccess { data -> + _roomDetail.value = data + _currentButtonType.value = data.buttonType + } + .onFailure { error -> + // 에러 처리 (필요시 에러 상태 추가) + } + + _isLoading.value = false + } + } + + fun onParticipationClick() { + viewModelScope.launch { + // TODO: 실제 참여하기 API 호출 + // 현재는 mock 로직으로 처리 + _currentButtonType.value = GroupBottomButtonType.CANCEL + showToastMessage("참여가 완료되었습니다") + } + } + + fun onCancelParticipationClick(dialogTitle: String, dialogDescription: String) { + // 참여 취소 확인 다이얼로그 표시 + _dialogTitle.value = dialogTitle + _dialogDescription.value = dialogDescription + pendingAction = { + viewModelScope.launch { + // TODO: 실제 참여 취소 API 호출 + // 현재는 mock 로직으로 처리 + _toastMessage.value = "모임방 참여가 취소되었어요! 다른 방을 찾아보세요." + _shouldNavigateToGroupScreen.value = true + } + } + _showDialog.value = true + } + + fun onCloseRecruitmentClick(dialogTitle: String, dialogDescription: String) { + // 모집 마감 확인 다이얼로그 표시 + _dialogTitle.value = dialogTitle + _dialogDescription.value = dialogDescription + pendingAction = { + viewModelScope.launch { + // TODO: 실제 모집 마감 API 호출 + // 현재는 mock 로직으로 처리 + showToastMessage("모집이 마감되었습니다") + } + } + _showDialog.value = true + } + + fun onDialogConfirm() { + _showDialog.value = false + pendingAction?.invoke() + pendingAction = null + } + + fun onDialogCancel() { + _showDialog.value = false + pendingAction = null + } + + fun hideToast() { + _showToast.value = false + } + + fun onNavigatedToGroupScreen() { + _shouldNavigateToGroupScreen.value = false + } + + private fun showToastMessage(message: String) { + _toastMessage.value = message + _showToast.value = true + } +} \ No newline at end of file From b184d3b983eda66b88b9e0b17957c0e437a162b8 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 10:20:38 +0900 Subject: [PATCH 39/68] =?UTF-8?q?[feat]:=20=EC=B0=B8=EC=97=AC=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=EB=A5=BC=20=EB=88=8C=EB=A0=80=EC=9D=84=20=EB=95=8C=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=8F=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/group/screen/GroupScreen.kt | 26 +++++++++++++++++++ .../thip/ui/group/viewmodel/GroupViewModel.kt | 15 +++++++++++ .../navigator/navigations/GroupNavigation.kt | 24 +++++++++++------ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index 73d89dc6..84730e54 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -13,15 +13,19 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect 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.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.FloatingButton +import com.texthip.thip.ui.common.modal.ToastWithDate import com.texthip.thip.ui.common.topappbar.LogoTopAppBar import com.texthip.thip.ui.group.myroom.component.GroupMySectionHeader import com.texthip.thip.ui.group.myroom.component.GroupPager @@ -30,6 +34,7 @@ import com.texthip.thip.ui.group.myroom.component.GroupSearchTextField import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors +import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -49,6 +54,8 @@ fun GroupScreen( val scrollState = rememberScrollState() val isRefreshing by viewModel.isRefreshing.collectAsState() val roomSectionsError by viewModel.roomSectionsError.collectAsState() + val showToast by viewModel.showToast.collectAsState() + val toastMessage by viewModel.toastMessage.collectAsState() Box( modifier = Modifier.fillMaxSize() @@ -128,6 +135,25 @@ fun GroupScreen( icon = painterResource(id = R.drawable.ic_makegroup), onClick = onNavigateToMakeRoom ) + + // 토스트 팝업 + if (showToast) { + ToastWithDate( + message = toastMessage, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 20.dp, vertical = 16.dp) + .zIndex(2f) + ) + } + } + + // 토스트 3초 후 자동 숨김 - showToast가 true가 된 시점부터 카운트 + LaunchedEffect(showToast) { + if (showToast) { + delay(3000L) + viewModel.hideToast() + } } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 6c4f2816..99d5d890 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -64,6 +64,12 @@ class GroupViewModel @Inject constructor( private val _selectedGenreIndex = MutableStateFlow(0) val selectedGenreIndex: StateFlow = _selectedGenreIndex.asStateFlow() + private val _showToast = MutableStateFlow(false) + val showToast: StateFlow = _showToast.asStateFlow() + + private val _toastMessage = MutableStateFlow("") + val toastMessage: StateFlow = _toastMessage.asStateFlow() + init { loadInitialData() } @@ -272,5 +278,14 @@ class GroupViewModel @Inject constructor( suspend fun getRoomRecruiting(roomId: Int): Result { return repository.getRoomRecruiting(roomId) } + + fun showToastMessage(message: String) { + _toastMessage.value = message + _showToast.value = true + } + + fun hideToast() { + _showToast.value = false + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index f0aab292..124c8f34 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -1,6 +1,7 @@ package com.texthip.thip.ui.navigator.navigations import android.annotation.SuppressLint +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.hilt.navigation.compose.hiltViewModel @@ -39,6 +40,16 @@ fun NavGraphBuilder.groupNavigation( composable { backStackEntry -> val groupViewModel: GroupViewModel = hiltViewModel() + // 네비게이션 파라미터로 전달된 토스트 메시지가 있는지 확인 + LaunchedEffect(Unit) { + val toastMessage = backStackEntry.savedStateHandle.get("toast_message") + + toastMessage?.let { message -> + backStackEntry.savedStateHandle.remove("toast_message") + groupViewModel.showToastMessage(message) + } + } + GroupScreen( viewModel = groupViewModel, onNavigateToMakeRoom = { @@ -138,14 +149,11 @@ fun NavGraphBuilder.groupNavigation( onRecommendationClick = { recommendation -> navController.navigateToRecommendedGroupRecruit(recommendation.id) }, - onParticipation = { - // TODO: 참여 API 호출 - }, - onCancelParticipation = { - // TODO: 참여 취소 API 호출 - }, - onCloseRecruitment = { - // TODO: 모집 마감 API 호출 + onNavigateToGroupScreen = { toastMessage -> + // GroupScreen에 토스트 메시지 전달 + val groupEntry = navController.getBackStackEntry(MainTabRoutes.Group) + groupEntry.savedStateHandle["toast_message"] = toastMessage + navController.popBackStack(MainTabRoutes.Group, false) }, onBackClick = { navigateBack() From 822b41e5ccd2b05c1eeb128e670c807f3a5da9c3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 10:40:44 +0900 Subject: [PATCH 40/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC,=20=EC=B7=A8=EC=86=8C=20DTO=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/model/group/request/RoomJoinRequest.kt | 9 +++++++++ .../thip/data/model/group/response/RoomJoinResponse.kt | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt create mode 100644 app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt b/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt new file mode 100644 index 00000000..636ff66b --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.group.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomJoinRequest( + @SerialName("type") val type: String // "join" 또는 "cancel" +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt new file mode 100644 index 00000000..31d65a03 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt @@ -0,0 +1,10 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomJoinResponse( + @SerialName("roomId") val roomId: Int, + @SerialName("type") val type: String // "join" 또는 "cancel" +) \ No newline at end of file From 31279fd8740b4f6b75ab295ef05a37afed0ad074 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 10:41:01 +0900 Subject: [PATCH 41/68] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC,=20=EC=B7=A8=EC=86=8C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/repository/GroupRepository.kt | 15 ++++++++++ .../thip/data/model/service/GroupService.kt | 8 +++++ .../viewmodel/GroupRoomRecruitViewModel.kt | 29 ++++++++++++++----- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 32077d11..b54ca45f 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -4,6 +4,7 @@ import android.content.Context import com.texthip.thip.R import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.model.group.request.RoomJoinRequest import com.texthip.thip.data.model.group.response.PaginationResult import com.texthip.thip.data.model.group.response.RoomMainResponse import com.texthip.thip.data.model.service.GroupService @@ -269,4 +270,18 @@ class GroupRepository @Inject constructor( } } + // 모임방 참여/취소 API 연동 + suspend fun joinOrCancelRoom(roomId: Int, type: String): Result { + return try { + val request = RoomJoinRequest(type = type) + groupService.joinOrCancelRoom(roomId, request) + .handleBaseResponse() + .mapCatching { response -> + response?.type ?: throw Exception("참여/취소 처리 실패: 응답이 없습니다") + } + } catch (e: Exception) { + Result.failure(e) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index 2b0c51cd..17817da4 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -3,9 +3,11 @@ package com.texthip.thip.data.model.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.book.response.BookListResponse import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.model.group.request.RoomJoinRequest import com.texthip.thip.data.model.group.response.CreateRoomResponse import com.texthip.thip.data.model.group.response.JoinedRoomListResponse import com.texthip.thip.data.model.group.response.MyRoomListResponse +import com.texthip.thip.data.model.group.response.RoomJoinResponse import com.texthip.thip.data.model.group.response.RoomRecruitingResponse import com.texthip.thip.data.model.group.response.RoomMainList import retrofit2.http.Body @@ -40,4 +42,10 @@ interface GroupService { @Body request: CreateRoomRequest ): BaseResponse + @POST("rooms/{roomId}/join") + suspend fun joinOrCancelRoom( + @Path("roomId") roomId: Int, + @Body request: RoomJoinRequest + ): BaseResponse + } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index 1b94d597..2768ae6a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -65,10 +65,16 @@ class GroupRoomRecruitViewModel @Inject constructor( fun onParticipationClick() { viewModelScope.launch { - // TODO: 실제 참여하기 API 호출 - // 현재는 mock 로직으로 처리 - _currentButtonType.value = GroupBottomButtonType.CANCEL - showToastMessage("참여가 완료되었습니다") + val roomId = _roomDetail.value?.id ?: return@launch + + repository.joinOrCancelRoom(roomId, "join") + .onSuccess { + _currentButtonType.value = GroupBottomButtonType.CANCEL + showToastMessage("참여가 완료되었습니다") + } + .onFailure { error -> + showToastMessage("참여 처리 중 오류가 발생했습니다: ${error.message}") + } } } @@ -78,10 +84,17 @@ class GroupRoomRecruitViewModel @Inject constructor( _dialogDescription.value = dialogDescription pendingAction = { viewModelScope.launch { - // TODO: 실제 참여 취소 API 호출 - // 현재는 mock 로직으로 처리 - _toastMessage.value = "모임방 참여가 취소되었어요! 다른 방을 찾아보세요." - _shouldNavigateToGroupScreen.value = true + val roomId = _roomDetail.value?.id ?: return@launch + + repository.joinOrCancelRoom(roomId, "cancel") + .onSuccess { + _currentButtonType.value = GroupBottomButtonType.JOIN + _toastMessage.value = "모임방 참여가 취소되었어요! 다른 방을 찾아보세요." + _shouldNavigateToGroupScreen.value = true + } + .onFailure { error -> + showToastMessage("참여 취소 중 오류가 발생했습니다: ${error.message}") + } } } _showDialog.value = true From d81d8c3cc4de739b6447c9408711eff5a090e970 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 11:21:53 +0900 Subject: [PATCH 42/68] =?UTF-8?q?[refactor]:=20=EC=95=88=EC=93=B0=EB=8A=94?= =?UTF-8?q?=20StateFlow=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EC=9E=A5?= =?UTF-8?q?=EB=A5=B4=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/repository/GroupRepository.kt | 27 ++-- .../makeroom/screen/GroupMakeRoomScreen.kt | 2 +- .../viewmodel/GroupMakeRoomViewModel.kt | 31 ++-- .../thip/ui/group/viewmodel/GroupViewModel.kt | 135 +++++------------- 4 files changed, 71 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index b54ca45f..47b8a192 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -32,6 +32,10 @@ class GroupRepository @Inject constructor( context.getString(R.string.humanities), context.getString(R.string.art) ) + + fun getGenres(): Result> { + return Result.success(genres) + } private var cachedUserName: String? = null // UI 장르명 → API 카테고리명 매핑 @@ -43,12 +47,8 @@ class GroupRepository @Inject constructor( } fun getUserName(): Result { - return try { - val name = cachedUserName ?: "사용자" // 캐시된 이름이 없으면 기본값 - Result.success(name) - } catch (e: Exception) { - Result.failure(e) - } + val name = cachedUserName ?: "사용자" // 캐시된 이름이 없으면 기본값 + return Result.success(name) } suspend fun getMyJoinedRooms(page: Int): Result> { return try { @@ -178,19 +178,10 @@ class GroupRepository @Inject constructor( } } + // TODO: 실제 검색 API 엔드포인트로 대체 필요 suspend fun getSearchGroups(): Result> { - return try { - // 기존에 로드된 섹션 데이터들을 합쳐서 반환 - val sectionsResult = getRoomSections() - if (sectionsResult.isSuccess) { - val allRooms = sectionsResult.getOrThrow().flatMap { it.rooms } - Result.success(allRooms) - } else { - Result.failure(sectionsResult.exceptionOrNull() ?: Exception("Failed to load search groups")) - } - } catch (e: Exception) { - Result.failure(e) - } + // 현재 더미 데이터 - API 연결 전까지 빈 리스트 반환 + return Result.success(emptyList()) } // 모집중인 모임방 상세 정보 API 연동 diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt index d06d6a50..91bb814b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt @@ -55,7 +55,7 @@ fun GroupMakeRoomScreen( val groupBooks by viewModel.groupBooks.collectAsState() val isLoadingBooks by viewModel.isLoadingBooks.collectAsState() val scrollState = rememberScrollState() - val genres = viewModel.genres + val genres by viewModel.genres.collectAsState() // 에러 메시지 표시 LaunchedEffect(uiState.errorMessage) { diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index bba8088a..cf2e731b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -36,7 +36,21 @@ class GroupMakeRoomViewModel @Inject constructor( private val _isLoadingBooks = MutableStateFlow(false) val isLoadingBooks: StateFlow = _isLoadingBooks.asStateFlow() - val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") + private val _genres = MutableStateFlow>(emptyList()) + val genres: StateFlow> = _genres.asStateFlow() + + init { + loadGenres() + } + + private fun loadGenres() { + viewModelScope.launch { + groupRepository.getGenres() + .onSuccess { genresList -> + _genres.value = genresList + } + } + } // 책 선택 fun selectBook(book: BookData) { @@ -180,14 +194,15 @@ class GroupMakeRoomViewModel @Inject constructor( // 장르 인덱스를 API 카테고리명으로 변환 private fun getApiCategoryName(genreIndex: Int): String { - return when (genreIndex) { - 0 -> "문학" - 1 -> "과학/IT" - 2 -> "사회과학" - 3 -> "인문학" - 4 -> "예술" - else -> "문학" // 기본값 + val currentGenres = _genres.value + if (genreIndex >= 0 && genreIndex < currentGenres.size) { + val genre = currentGenres[genreIndex] + return when (genre) { + "과학·IT" -> "과학/IT" + else -> genre + } } + return "문학" // 기본값 } // 에러 메시지 클리어 diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 99d5d890..9b714433 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -5,15 +5,14 @@ import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import javax.inject.Inject @HiltViewModel @@ -44,22 +43,12 @@ class GroupViewModel @Inject constructor( private val _roomSectionsError = MutableStateFlow(null) val roomSectionsError: StateFlow = _roomSectionsError.asStateFlow() - private val _userName = MutableStateFlow("") val userName: StateFlow = _userName.asStateFlow() - private val _doneGroups = MutableStateFlow>(emptyList()) - val doneGroups: StateFlow> = _doneGroups.asStateFlow() - - private val _myRoomGroups = MutableStateFlow>(emptyList()) - val myRoomGroups: StateFlow> = _myRoomGroups.asStateFlow() - private val _searchGroups = MutableStateFlow>(emptyList()) val searchGroups: StateFlow> = _searchGroups.asStateFlow() - - private val _genres = MutableStateFlow>(emptyList()) - val genres: StateFlow> = _genres.asStateFlow() private val _selectedGenreIndex = MutableStateFlow(0) val selectedGenreIndex: StateFlow = _selectedGenreIndex.asStateFlow() @@ -78,7 +67,6 @@ class GroupViewModel @Inject constructor( loadUserName() loadMyGroups() loadRoomSections() - loadMyRoomGroups() loadSearchGroups() } @@ -93,22 +81,19 @@ class GroupViewModel @Inject constructor( fun loadMyGroups(reset: Boolean = false) = viewModelScope.launch { if (reset) { - currentMyGroupsPage = 1 - loadedPagesCount = 0 - _myGroups.value = emptyList() - _hasMoreMyGroups.value = true + resetMyGroupsData() _isRefreshing.value = true } try { // Pager를 위한 초기 배치 로드 - loadPageBatch() + loadPageBatchSuspend() } finally { _isRefreshing.value = false } } - private fun loadPageBatch() = viewModelScope.launch { - if (!_hasMoreMyGroups.value || isBatchLoading) return@launch + private suspend fun loadPageBatchSuspend() { + if (!_hasMoreMyGroups.value || isBatchLoading) return try { isBatchLoading = true @@ -139,6 +124,10 @@ class GroupViewModel @Inject constructor( } } + private fun loadPageBatch() = viewModelScope.launch { + loadPageBatchSuspend() + } + // GroupPager에서 현재 카드 인덱스를 알려주면 미리 로드 판단 fun onCardVisible(cardIndex: Int) { val totalCards = _myGroups.value.size @@ -155,10 +144,16 @@ class GroupViewModel @Inject constructor( viewModelScope.launch { _roomSectionsError.value = null - val currentGenres = _genres.value + // Repository에서 직접 장르 목록 가져오기 + val genresResult = repository.getGenres() val selectedIndex = _selectedGenreIndex.value - val selectedGenre = if (currentGenres.isNotEmpty() && selectedIndex >= 0 && selectedIndex < currentGenres.size) { - currentGenres[selectedIndex] + val selectedGenre = if (genresResult.isSuccess) { + val genres = genresResult.getOrThrow() + if (selectedIndex >= 0 && selectedIndex < genres.size) { + genres[selectedIndex] + } else { + genres.firstOrNull() ?: "문학" // 기본값 + } } else { "문학" // 기본값 } @@ -174,19 +169,17 @@ class GroupViewModel @Inject constructor( } fun selectGenre(genreIndex: Int) { - if (genreIndex >= 0 && genreIndex != _selectedGenreIndex.value) { - _selectedGenreIndex.value = genreIndex - loadRoomSections() // 장르 변경 시 새로운 데이터 로드 + // Repository에서 직접 장르 목록 확인 + val genresResult = repository.getGenres() + if (genresResult.isSuccess) { + val genres = genresResult.getOrThrow() + if (genreIndex >= 0 && genreIndex < genres.size && genreIndex != _selectedGenreIndex.value) { + _selectedGenreIndex.value = genreIndex + loadRoomSections() // 장른 변경 시 새로운 데이터 로드 + } } } - private fun loadMyRoomGroups() { - viewModelScope.launch { - // getMyRoomGroups() 제거됨 - API 연결 완료 후 실제 API 사용 예정 - // 현재는 빈 리스트로 설정 - _myRoomGroups.value = emptyList() - } - } private fun loadSearchGroups() { viewModelScope.launch { @@ -203,68 +196,14 @@ class GroupViewModel @Inject constructor( try { // 모든 데이터를 병렬로 새로고침하고 완료를 기다림 val jobs = listOf( - async { - repository.getUserName() - .onSuccess { userName -> - _userName.value = userName - } - }, - async { + async { loadUserName() }, + async { // 내 모임방 데이터 리셋 후 로드 - currentMyGroupsPage = 1 - loadedPagesCount = 0 - _myGroups.value = emptyList() - _hasMoreMyGroups.value = true - - // 첫 번째 배치 로드 (3페이지) - if (!_hasMoreMyGroups.value || isBatchLoading) return@async - - try { - isBatchLoading = true - val currentBatchStart = currentMyGroupsPage - val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 - - for (page in currentBatchStart..batchEndPage) { - if (!_hasMoreMyGroups.value) break - - repository.getMyJoinedRooms(page) - .onSuccess { paginationResult -> - _myGroups.value = _myGroups.value + paginationResult.data - _hasMoreMyGroups.value = paginationResult.hasMore - loadedPagesCount++ - currentMyGroupsPage = page + 1 - } - .onFailure { - break - } - } - } finally { - isBatchLoading = false - } - }, - async { - _roomSectionsError.value = null - - val currentGenres = _genres.value - val selectedIndex = _selectedGenreIndex.value - val selectedGenre = if (currentGenres.isNotEmpty() && selectedIndex >= 0 && selectedIndex < currentGenres.size) { - currentGenres[selectedIndex] - } else { - "문학" // 기본값 - } - - repository.getRoomSections(selectedGenre) - .onSuccess { sections -> - _roomSections.value = sections - } - .onFailure { error -> - _roomSectionsError.value = error.message - } + resetMyGroupsData() + loadPageBatchSuspend() }, - async { - // getMyRoomGroups() 제거됨 - API 연결 완료 후 실제 API 사용 예정 - _myRoomGroups.value = emptyList() - } + async { loadRoomSections() }, + async { loadSearchGroups() } ) jobs.awaitAll() @@ -273,10 +212,12 @@ class GroupViewModel @Inject constructor( } } } - - suspend fun getRoomRecruiting(roomId: Int): Result { - return repository.getRoomRecruiting(roomId) + private fun resetMyGroupsData() { + currentMyGroupsPage = 1 + loadedPagesCount = 0 + _myGroups.value = emptyList() + _hasMoreMyGroups.value = true } fun showToastMessage(message: String) { From 4b3f371244c136ce8a6912a957375f1b76c532a6 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 11:25:38 +0900 Subject: [PATCH 43/68] =?UTF-8?q?[refactor]:=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A7=84=EC=9E=85=EC=8B=9C=20=ED=95=AD?= =?UTF-8?q?=EC=83=81=20=EC=B5=9C=EC=8B=A0=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=B3=BC=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/group/screen/GroupScreen.kt | 4 ++ .../thip/ui/group/viewmodel/GroupViewModel.kt | 55 ++++++++++--------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index 84730e54..ac0953d8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -48,6 +48,10 @@ fun GroupScreen( onNavigateToGroupRoom: (Int) -> Unit = {}, // 기록장 화면으로 이동 viewModel: GroupViewModel = hiltViewModel() ) { + // 화면 재진입 시 데이터 새로고침 + LaunchedEffect(Unit) { + viewModel.refreshDataOnScreenEnter() + } val myGroups by viewModel.myGroups.collectAsState() val roomSections by viewModel.roomSections.collectAsState() val selectedGenreIndex by viewModel.selectedGenreIndex.collectAsState() diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 9b714433..24e9c559 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -22,13 +22,13 @@ class GroupViewModel @Inject constructor( private val _myGroups = MutableStateFlow>(emptyList()) val myGroups: StateFlow> = _myGroups - + private val _hasMoreMyGroups = MutableStateFlow(true) val hasMoreMyGroups: StateFlow = _hasMoreMyGroups.asStateFlow() - + private val _isRefreshing = MutableStateFlow(false) val isRefreshing: StateFlow = _isRefreshing.asStateFlow() - + private val _isLoadingMoreMyGroups = MutableStateFlow(false) val isLoadingMoreMyGroups: StateFlow = _isLoadingMoreMyGroups.asStateFlow() @@ -69,7 +69,7 @@ class GroupViewModel @Inject constructor( loadRoomSections() loadSearchGroups() } - + private fun loadUserName() { viewModelScope.launch { repository.getUserName() @@ -91,21 +91,21 @@ class GroupViewModel @Inject constructor( _isRefreshing.value = false } } - + private suspend fun loadPageBatchSuspend() { if (!_hasMoreMyGroups.value || isBatchLoading) return - + try { isBatchLoading = true _isLoadingMoreMyGroups.value = true - + // 3페이지씩 배치로 로드 (Pager 미리보기용) val currentBatchStart = currentMyGroupsPage val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 - + for (page in currentBatchStart..batchEndPage) { if (!_hasMoreMyGroups.value) break - + repository.getMyJoinedRooms(page) .onSuccess { paginationResult -> _myGroups.value = _myGroups.value + paginationResult.data @@ -123,19 +123,20 @@ class GroupViewModel @Inject constructor( _isLoadingMoreMyGroups.value = false } } - + private fun loadPageBatch() = viewModelScope.launch { loadPageBatchSuspend() } - + // GroupPager에서 현재 카드 인덱스를 알려주면 미리 로드 판단 fun onCardVisible(cardIndex: Int) { val totalCards = _myGroups.value.size val currentPageEquivalent = (cardIndex / 3) + 1 // 3개씩 한 페이지라고 가정 - + // 현재 로드된 페이지의 임계점에 도달하면 다음 배치 로드 - if (currentPageEquivalent >= loadedPagesCount - PRELOAD_THRESHOLD && - _hasMoreMyGroups.value && !isBatchLoading) { + if (currentPageEquivalent >= loadedPagesCount - PRELOAD_THRESHOLD && + _hasMoreMyGroups.value && !isBatchLoading + ) { loadPageBatch() } } @@ -143,7 +144,7 @@ class GroupViewModel @Inject constructor( private fun loadRoomSections() { viewModelScope.launch { _roomSectionsError.value = null - + // Repository에서 직접 장르 목록 가져오기 val genresResult = repository.getGenres() val selectedIndex = _selectedGenreIndex.value @@ -157,7 +158,7 @@ class GroupViewModel @Inject constructor( } else { "문학" // 기본값 } - + repository.getRoomSections(selectedGenre) .onSuccess { sections -> _roomSections.value = sections @@ -167,7 +168,7 @@ class GroupViewModel @Inject constructor( } } } - + fun selectGenre(genreIndex: Int) { // Repository에서 직접 장르 목록 확인 val genresResult = repository.getGenres() @@ -179,8 +180,8 @@ class GroupViewModel @Inject constructor( } } } - - + + private fun loadSearchGroups() { viewModelScope.launch { repository.getSearchGroups() @@ -189,7 +190,7 @@ class GroupViewModel @Inject constructor( } } } - + fun refreshGroupData() { viewModelScope.launch { _isRefreshing.value = true @@ -197,7 +198,7 @@ class GroupViewModel @Inject constructor( // 모든 데이터를 병렬로 새로고침하고 완료를 기다림 val jobs = listOf( async { loadUserName() }, - async { + async { // 내 모임방 데이터 리셋 후 로드 resetMyGroupsData() loadPageBatchSuspend() @@ -205,28 +206,32 @@ class GroupViewModel @Inject constructor( async { loadRoomSections() }, async { loadSearchGroups() } ) - + jobs.awaitAll() } finally { _isRefreshing.value = false } } } - + private fun resetMyGroupsData() { currentMyGroupsPage = 1 loadedPagesCount = 0 _myGroups.value = emptyList() _hasMoreMyGroups.value = true } - + fun showToastMessage(message: String) { _toastMessage.value = message _showToast.value = true } - + fun hideToast() { _showToast.value = false } + fun refreshDataOnScreenEnter() { + refreshGroupData() + } + } \ No newline at end of file From 35a59a86dd5132457e5d0dcbd823288e0e19b64d Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 16:05:39 +0900 Subject: [PATCH 44/68] =?UTF-8?q?[chore]:=20=EC=A3=BC=EC=84=9D=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/group/request/CreateRoomRequest.kt | 8 ++--- .../model/group/request/RoomJoinRequest.kt | 2 +- .../model/group/response/RoomJoinResponse.kt | 2 +- .../data/model/repository/GroupRepository.kt | 29 ++++++------------- .../thip/data/model/service/BookService.kt | 2 +- .../thip/data/model/service/GroupService.kt | 4 +-- 6 files changed, 18 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt b/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt index 6df73607..1cfbe6b2 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt @@ -8,9 +8,9 @@ data class CreateRoomRequest( val category: String, val roomName: String, val description: String, - val progressStartDate: String, // yyyy.MM.dd 형식 - val progressEndDate: String, // yyyy.MM.dd 형식 - val recruitCount: Int, // 1~30명 - val password: String? = null, // 비공개방일 때만 필요 (숫자 4자리) + val progressStartDate: String, + val progressEndDate: String, + val recruitCount: Int, + val password: String? = null, val isPublic: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt b/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt index 636ff66b..3e545262 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.Serializable @Serializable data class RoomJoinRequest( - @SerialName("type") val type: String // "join" 또는 "cancel" + @SerialName("type") val type: String ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt index 31d65a03..2af96a65 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable @Serializable data class RoomJoinResponse( @SerialName("roomId") val roomId: Int, - @SerialName("type") val type: String // "join" 또는 "cancel" + @SerialName("type") val type: String ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt index 47b8a192..a002a93a 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt @@ -38,7 +38,6 @@ class GroupRepository @Inject constructor( } private var cachedUserName: String? = null - // UI 장르명 → API 카테고리명 매핑 private fun mapGenreToApiCategory(genre: String): String { return when (genre) { "과학·IT" -> "과학/IT" @@ -47,7 +46,7 @@ class GroupRepository @Inject constructor( } fun getUserName(): Result { - val name = cachedUserName ?: "사용자" // 캐시된 이름이 없으면 기본값 + val name = cachedUserName ?: "사용자" return Result.success(name) } suspend fun getMyJoinedRooms(page: Int): Result> { @@ -56,7 +55,6 @@ class GroupRepository @Inject constructor( .handleBaseResponse() .mapCatching { data -> data?.let { joinedRoomsDto -> - // API 응답에서 받은 닉네임을 캐시에 저장 cachedUserName = joinedRoomsDto.nickname val groups = joinedRoomsDto.roomList.map { dto -> @@ -120,7 +118,6 @@ class GroupRepository @Inject constructor( } } - // 완료된 모임방 API 연동 suspend fun getMyRoomsByType(type: String?, cursor: String? = null): Result { return try { groupService.getMyRooms(type, cursor) @@ -174,24 +171,21 @@ class GroupRepository @Inject constructor( deadlineDate.contains("일 뒤") -> { deadlineDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 } - else -> 0 // 파싱할 수 없는 경우 0 반환 + else -> 0 } } // TODO: 실제 검색 API 엔드포인트로 대체 필요 suspend fun getSearchGroups(): Result> { - // 현재 더미 데이터 - API 연결 전까지 빈 리스트 반환 return Result.success(emptyList()) } - // 모집중인 모임방 상세 정보 API 연동 suspend fun getRoomRecruiting(roomId: Int): Result { return try { groupService.getRoomRecruiting(roomId) .handleBaseResponse() .mapCatching { recruitingDto -> recruitingDto?.let { data -> - // 책 정보 변환 val bookData = GroupBookData( title = data.bookTitle, author = data.authorName, @@ -200,21 +194,19 @@ class GroupRepository @Inject constructor( imageUrl = data.bookImageUrl ) - // 추천 모임방 변환 val recommendations = data.recommendRooms.map { recommendDto -> GroupCardItemRoomData( - id = recommendDto.roomId, // API에서 제공하는 실제 roomId + id = recommendDto.roomId, title = recommendDto.roomName, participants = recommendDto.memberCount, maxParticipants = recommendDto.recruitCount, isRecruiting = true, endDate = extractDaysFromDeadline(recommendDto.recruitEndDate), imageUrl = recommendDto.roomImageUrl, - genreIndex = 0, // 기본값 + genreIndex = 0, ) } - // GroupRoomData로 변환 GroupRoomData( id = data.roomId, title = data.roomName, @@ -239,36 +231,33 @@ class GroupRepository @Inject constructor( } } - // 버튼 타입 결정 로직 private fun determineButtonType(isHost: Boolean, isJoining: Boolean): GroupBottomButtonType { return when { - isHost -> GroupBottomButtonType.CLOSE // 호스트는 모집 마감 가능 - isJoining -> GroupBottomButtonType.CANCEL // 참여 중이면 취소 가능 - else -> GroupBottomButtonType.JOIN // 참여하지 않았으면 참여 가능 + isHost -> GroupBottomButtonType.CLOSE + isJoining -> GroupBottomButtonType.CANCEL + else -> GroupBottomButtonType.JOIN } } - // 모임방 생성 API 연동 suspend fun createRoom(request: CreateRoomRequest): Result { return try { groupService.createRoom(request) .handleBaseResponse() .mapCatching { createRoomResponse -> - createRoomResponse?.roomId ?: throw Exception("방 생성 실패: roomId가 없습니다") + createRoomResponse?.roomId ?: throw Exception("Failed to create room: roomId is null") } } catch (e: Exception) { Result.failure(e) } } - // 모임방 참여/취소 API 연동 suspend fun joinOrCancelRoom(roomId: Int, type: String): Result { return try { val request = RoomJoinRequest(type = type) groupService.joinOrCancelRoom(roomId, request) .handleBaseResponse() .mapCatching { response -> - response?.type ?: throw Exception("참여/취소 처리 실패: 응답이 없습니다") + response?.type ?: throw Exception("Failed to join/cancel room: no response") } } catch (e: Exception) { Result.failure(e) diff --git a/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt b/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt index 7e38e607..c7ad24d0 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt @@ -9,6 +9,6 @@ interface BookService { @GET("books") suspend fun getBooks( - @Query("type") type: String // "saved" 또는 "joining" + @Query("type") type: String ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt index 17817da4..86e11c14 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt @@ -25,12 +25,12 @@ interface GroupService { @GET("rooms") suspend fun getRooms( - @Query("category") category: String = "문학" // 디폴트=문학 + @Query("category") category: String = "문학" ): BaseResponse @GET("rooms/my") suspend fun getMyRooms( - @Query("type") type: String? = null, // "playingAndRecruiting", "recruiting", "playing", "expired" + @Query("type") type: String? = null, @Query("cursor") cursor: String? = null ): BaseResponse From 1fe49475c222418972ee10d8c3321764ceec958c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 16:19:01 +0900 Subject: [PATCH 45/68] =?UTF-8?q?[chore]:=20=EC=A3=BC=EC=84=9D=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../done/viewmodel/GroupDoneViewModel.kt | 2 - .../viewmodel/GroupMakeRoomViewModel.kt | 21 +--------- .../myroom/viewmodel/GroupMyViewModel.kt | 1 - .../viewmodel/GroupRoomRecruitViewModel.kt | 4 -- .../thip/ui/group/viewmodel/GroupViewModel.kt | 38 ++++--------------- 5 files changed, 9 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index 8077c6a1..a1ef86f3 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -70,10 +70,8 @@ class GroupDoneViewModel @Inject constructor( isLastPage = paginationResult.isLast } .onFailure { - // 에러 발생 처리 } } finally { - // 성공/실패 관계없이 로딩 상태는 항상 해제 _isLoading.value = false isLoadingMore = false } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index cf2e731b..39cdcd50 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -26,7 +26,6 @@ class GroupMakeRoomViewModel @Inject constructor( private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) val uiState: StateFlow = _uiState.asStateFlow() - // 책 목록 상태 private val _savedBooks = MutableStateFlow>(emptyList()) val savedBooks: StateFlow> = _savedBooks.asStateFlow() @@ -52,12 +51,10 @@ class GroupMakeRoomViewModel @Inject constructor( } } - // 책 선택 fun selectBook(book: BookData) { _uiState.value = _uiState.value.copy(selectedBook = book) } - // 책 검색 시트 표시 상태 변경 fun toggleBookSearchSheet(show: Boolean) { _uiState.value = _uiState.value.copy(showBookSearchSheet = show) if (show) { @@ -65,12 +62,10 @@ class GroupMakeRoomViewModel @Inject constructor( } } - // 책 목록 로드 private fun loadBooks() { viewModelScope.launch { _isLoadingBooks.value = true try { - // 저장한 책 로드 val savedBooksResult = bookRepository.getBooks("saved") savedBooksResult.onSuccess { bookDtos -> _savedBooks.value = bookDtos.map { it.toBookData() } @@ -78,7 +73,6 @@ class GroupMakeRoomViewModel @Inject constructor( _savedBooks.value = emptyList() } - // 모임 책 로드 val groupBooksResult = bookRepository.getBooks("joining") groupBooksResult.onSuccess { bookDtos -> _groupBooks.value = bookDtos.map { it.toBookData() } @@ -94,7 +88,6 @@ class GroupMakeRoomViewModel @Inject constructor( } } - // BookData로 변환 private fun BookSavedResponse.toBookData(): BookData { return BookData( title = this.bookTitle, @@ -104,22 +97,18 @@ class GroupMakeRoomViewModel @Inject constructor( ) } - // 장르 선택 fun selectGenre(index: Int) { _uiState.value = _uiState.value.copy(selectedGenreIndex = index) } - // 방 제목 변경 fun updateRoomTitle(title: String) { _uiState.value = _uiState.value.copy(roomTitle = title) } - // 방 설명 변경 fun updateRoomDescription(description: String) { _uiState.value = _uiState.value.copy(roomDescription = description) } - // 모임 날짜 범위 설정 fun setDateRange(startDate: LocalDate, endDate: LocalDate) { _uiState.value = _uiState.value.copy( meetingStartDate = startDate, @@ -127,12 +116,10 @@ class GroupMakeRoomViewModel @Inject constructor( ) } - // 인원 수 설정 fun setMemberLimit(count: Int) { _uiState.value = _uiState.value.copy(memberLimit = count) } - // 비밀방 설정 fun togglePrivate(isPrivate: Boolean) { _uiState.value = _uiState.value.copy( isPrivate = isPrivate, @@ -140,12 +127,10 @@ class GroupMakeRoomViewModel @Inject constructor( ) } - // 비밀번호 설정 fun updatePassword(password: String) { _uiState.value = _uiState.value.copy(password = password) } - // 그룹 생성 요청 fun createGroup(onSuccess: (Int) -> Unit, onError: (String) -> Unit) { val currentState = _uiState.value @@ -164,7 +149,6 @@ class GroupMakeRoomViewModel @Inject constructor( try { _uiState.value = currentState.copy(isLoading = true, errorMessage = null) - // API 요청 데이터 생성 val request = CreateRoomRequest( isbn = selectedBook.isbn, category = getApiCategoryName(currentState.selectedGenreIndex), @@ -177,7 +161,6 @@ class GroupMakeRoomViewModel @Inject constructor( isPublic = !currentState.isPrivate ) - // API 호출 val result = groupRepository.createRoom(request) result.onSuccess { roomId -> onSuccess(roomId) @@ -192,7 +175,6 @@ class GroupMakeRoomViewModel @Inject constructor( } } - // 장르 인덱스를 API 카테고리명으로 변환 private fun getApiCategoryName(genreIndex: Int): String { val currentGenres = _genres.value if (genreIndex >= 0 && genreIndex < currentGenres.size) { @@ -202,10 +184,9 @@ class GroupMakeRoomViewModel @Inject constructor( else -> genre } } - return "문학" // 기본값 + return "문학" } - // 에러 메시지 클리어 fun clearError() { _uiState.value = _uiState.value.copy(errorMessage = null) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt index b227f63a..9134fcd1 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -61,7 +61,6 @@ class GroupMyViewModel @Inject constructor( isLastPage = paginationResult.isLast } .onFailure { - // 에러 처리 (필요시 에러 상태 추가) } } finally { isLoadingData = false diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index 2768ae6a..58df1e42 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -56,7 +56,6 @@ class GroupRoomRecruitViewModel @Inject constructor( _currentButtonType.value = data.buttonType } .onFailure { error -> - // 에러 처리 (필요시 에러 상태 추가) } _isLoading.value = false @@ -79,7 +78,6 @@ class GroupRoomRecruitViewModel @Inject constructor( } fun onCancelParticipationClick(dialogTitle: String, dialogDescription: String) { - // 참여 취소 확인 다이얼로그 표시 _dialogTitle.value = dialogTitle _dialogDescription.value = dialogDescription pendingAction = { @@ -101,13 +99,11 @@ class GroupRoomRecruitViewModel @Inject constructor( } fun onCloseRecruitmentClick(dialogTitle: String, dialogDescription: String) { - // 모집 마감 확인 다이얼로그 표시 _dialogTitle.value = dialogTitle _dialogDescription.value = dialogDescription pendingAction = { viewModelScope.launch { // TODO: 실제 모집 마감 API 호출 - // 현재는 mock 로직으로 처리 showToastMessage("모집이 마감되었습니다") } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 24e9c559..46b7a8da 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -34,8 +34,8 @@ class GroupViewModel @Inject constructor( private var currentMyGroupsPage = 1 private var loadedPagesCount = 0 - private val PAGES_PER_BATCH = 3 // 5페이지에서 3페이지로 줄임 - private val PRELOAD_THRESHOLD = 2 // 임계점도 2로 줄임 + private val pagesPerBatch = 3 + private val preloadThreshold = 2 private var isBatchLoading = false private val _roomSections = MutableStateFlow>(emptyList()) @@ -47,8 +47,6 @@ class GroupViewModel @Inject constructor( private val _userName = MutableStateFlow("") val userName: StateFlow = _userName.asStateFlow() - private val _searchGroups = MutableStateFlow>(emptyList()) - val searchGroups: StateFlow> = _searchGroups.asStateFlow() private val _selectedGenreIndex = MutableStateFlow(0) val selectedGenreIndex: StateFlow = _selectedGenreIndex.asStateFlow() @@ -67,7 +65,6 @@ class GroupViewModel @Inject constructor( loadUserName() loadMyGroups() loadRoomSections() - loadSearchGroups() } private fun loadUserName() { @@ -85,7 +82,6 @@ class GroupViewModel @Inject constructor( _isRefreshing.value = true } try { - // Pager를 위한 초기 배치 로드 loadPageBatchSuspend() } finally { _isRefreshing.value = false @@ -99,9 +95,8 @@ class GroupViewModel @Inject constructor( isBatchLoading = true _isLoadingMoreMyGroups.value = true - // 3페이지씩 배치로 로드 (Pager 미리보기용) val currentBatchStart = currentMyGroupsPage - val batchEndPage = currentBatchStart + PAGES_PER_BATCH - 1 + val batchEndPage = currentBatchStart + pagesPerBatch - 1 for (page in currentBatchStart..batchEndPage) { if (!_hasMoreMyGroups.value) break @@ -114,7 +109,6 @@ class GroupViewModel @Inject constructor( currentMyGroupsPage = page + 1 } .onFailure { - // 에러 발생 시 배치 로딩 중단 break } } @@ -128,13 +122,10 @@ class GroupViewModel @Inject constructor( loadPageBatchSuspend() } - // GroupPager에서 현재 카드 인덱스를 알려주면 미리 로드 판단 fun onCardVisible(cardIndex: Int) { - val totalCards = _myGroups.value.size - val currentPageEquivalent = (cardIndex / 3) + 1 // 3개씩 한 페이지라고 가정 + val currentPageEquivalent = (cardIndex / 3) + 1 - // 현재 로드된 페이지의 임계점에 도달하면 다음 배치 로드 - if (currentPageEquivalent >= loadedPagesCount - PRELOAD_THRESHOLD && + if (currentPageEquivalent >= loadedPagesCount - preloadThreshold && _hasMoreMyGroups.value && !isBatchLoading ) { loadPageBatch() @@ -145,7 +136,6 @@ class GroupViewModel @Inject constructor( viewModelScope.launch { _roomSectionsError.value = null - // Repository에서 직접 장르 목록 가져오기 val genresResult = repository.getGenres() val selectedIndex = _selectedGenreIndex.value val selectedGenre = if (genresResult.isSuccess) { @@ -153,10 +143,10 @@ class GroupViewModel @Inject constructor( if (selectedIndex >= 0 && selectedIndex < genres.size) { genres[selectedIndex] } else { - genres.firstOrNull() ?: "문학" // 기본값 + genres.firstOrNull() ?: "문학" } } else { - "문학" // 기본값 + "문학" } repository.getRoomSections(selectedGenre) @@ -170,41 +160,29 @@ class GroupViewModel @Inject constructor( } fun selectGenre(genreIndex: Int) { - // Repository에서 직접 장르 목록 확인 val genresResult = repository.getGenres() if (genresResult.isSuccess) { val genres = genresResult.getOrThrow() if (genreIndex >= 0 && genreIndex < genres.size && genreIndex != _selectedGenreIndex.value) { _selectedGenreIndex.value = genreIndex - loadRoomSections() // 장른 변경 시 새로운 데이터 로드 + loadRoomSections() } } } - private fun loadSearchGroups() { - viewModelScope.launch { - repository.getSearchGroups() - .onSuccess { groups -> - _searchGroups.value = groups - } - } - } fun refreshGroupData() { viewModelScope.launch { _isRefreshing.value = true try { - // 모든 데이터를 병렬로 새로고침하고 완료를 기다림 val jobs = listOf( async { loadUserName() }, async { - // 내 모임방 데이터 리셋 후 로드 resetMyGroupsData() loadPageBatchSuspend() }, async { loadRoomSections() }, - async { loadSearchGroups() } ) jobs.awaitAll() From 31374cb83b04247f844e33db54f7d5e2871e1c4b Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 16:27:35 +0900 Subject: [PATCH 46/68] =?UTF-8?q?[refactor]:=20SerialName=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20DTO=EC=97=90=20=EC=B6=94=EA=B0=80=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/book/response/BookSavedResponse.kt | 13 +++++++------ .../model/group/request/CreateRoomRequest.kt | 19 ++++++++++--------- .../group/response/CreateRoomResponse.kt | 3 ++- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt b/app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt index 879a2282..9526091b 100644 --- a/app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt @@ -1,17 +1,18 @@ package com.texthip.thip.data.model.book.response +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class BookSavedResponse( - val isbn: String, - val bookTitle: String, - val authorName: String, - val publisher: String, - val imageUrl: String? + @SerialName("isbn") val isbn: String, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("authorName") val authorName: String, + @SerialName("publisher") val publisher: String, + @SerialName("imageUrl") val imageUrl: String? ) @Serializable data class BookListResponse( - val bookList: List + @SerialName("bookList") val bookList: List ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt b/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt index 1cfbe6b2..b8b88fd8 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt @@ -1,16 +1,17 @@ package com.texthip.thip.data.model.group.request +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class CreateRoomRequest( - val isbn: String, - val category: String, - val roomName: String, - val description: String, - val progressStartDate: String, - val progressEndDate: String, - val recruitCount: Int, - val password: String? = null, - val isPublic: Boolean + @SerialName("isbn") val isbn: String, + @SerialName("category") val category: String, + @SerialName("roomName") val roomName: String, + @SerialName("description") val description: String, + @SerialName("progressStartDate") val progressStartDate: String, + @SerialName("progressEndDate") val progressEndDate: String, + @SerialName("recruitCount") val recruitCount: Int, + @SerialName("password") val password: String? = null, + @SerialName("isPublic") val isPublic: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt index f681afe0..a1c57cf1 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt @@ -1,8 +1,9 @@ package com.texthip.thip.data.model.group.response +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class CreateRoomResponse( - val roomId: Int + @SerialName("roomId") val roomId: Int ) \ No newline at end of file From 8b691e266926e22d898e4afd603e21b4bb7f8db3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 16:44:43 +0900 Subject: [PATCH 47/68] =?UTF-8?q?[refactor]:=20GroupRepository=EC=9D=98=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=B6=84=EB=A6=AC(Mapper,=20Manager)=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/data/manager/GenreManager.kt | 44 +++++++ .../thip/data/manager/UserDataManager.kt | 26 ++++ .../thip/data/mapper/GroupDataMapper.kt | 118 ++++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt create mode 100644 app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt create mode 100644 app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt diff --git a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt new file mode 100644 index 00000000..9426906f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt @@ -0,0 +1,44 @@ +package com.texthip.thip.data.manager + +import android.content.Context +import com.texthip.thip.R +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GenreManager @Inject constructor( + @param:ApplicationContext private val context: Context +) { + + private val genres = listOf( + context.getString(R.string.literature), + context.getString(R.string.science_it), + context.getString(R.string.social_science), + context.getString(R.string.humanities), + context.getString(R.string.art) + ) + + fun getGenres(): List { + return genres + } + + fun mapGenreToApiCategory(genre: String): String { + return when (genre) { + "과학·IT" -> "과학/IT" + else -> genre + } + } + + fun getDefaultGenre(): String { + return context.getString(R.string.literature) + } + + fun isValidGenreIndex(index: Int): Boolean { + return index >= 0 && index < genres.size + } + + fun getGenreByIndex(index: Int): String? { + return if (isValidGenreIndex(index)) genres[index] else null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt b/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt new file mode 100644 index 00000000..e00ab505 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt @@ -0,0 +1,26 @@ +package com.texthip.thip.data.manager + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserDataManager @Inject constructor() { + + private var cachedUserName: String? = null + + fun getUserName(): String { + return cachedUserName ?: "사용자" + } + + fun cacheUserName(name: String) { + cachedUserName = name + } + + fun clearUserData() { + cachedUserName = null + } + + fun hasUserName(): Boolean { + return cachedUserName != null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt b/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt new file mode 100644 index 00000000..c21f07db --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt @@ -0,0 +1,118 @@ +package com.texthip.thip.data.mapper + +import com.texthip.thip.data.model.group.response.JoinedRoomResponse +import com.texthip.thip.data.model.group.response.MyRoomResponse +import com.texthip.thip.data.model.group.response.RecommendRoomResponse +import com.texthip.thip.data.model.group.response.RoomMainResponse +import com.texthip.thip.data.model.group.response.RoomRecruitingResponse +import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import com.texthip.thip.ui.group.myroom.mock.GroupBookData +import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType +import com.texthip.thip.ui.group.myroom.mock.GroupCardData +import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData +import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GroupDataMapper @Inject constructor() { + + fun toGroupCardData(dto: JoinedRoomResponse, nickname: String): GroupCardData { + return GroupCardData( + id = dto.roomId, + title = dto.bookTitle, + members = dto.memberCount, + imageUrl = dto.bookImageUrl, + progress = dto.userPercentage, + nickname = nickname + ) + } + + fun toGroupCardItemRoomData(dto: RoomMainResponse, daysLeft: Int): GroupCardItemRoomData { + return GroupCardItemRoomData( + id = dto.roomId, + title = dto.roomName, + participants = dto.memberCount, + maxParticipants = dto.recruitCount, + isRecruiting = true, + endDate = daysLeft, + imageUrl = dto.bookImageUrl, + genreIndex = 0, + isSecret = false + ) + } + + fun toGroupRoomData(dto: RoomRecruitingResponse): GroupRoomData { + val bookData = GroupBookData( + title = dto.bookTitle, + author = dto.authorName, + publisher = dto.publisher, + description = dto.bookDescription, + imageUrl = dto.bookImageUrl + ) + + val recommendations = dto.recommendRooms.map { recommendDto -> + toGroupCardItemRoomDataFromRecommend(recommendDto) + } + + return GroupRoomData( + id = dto.roomId, + title = dto.roomName, + isSecret = !dto.isPublic, + description = dto.roomDescription, + startDate = dto.progressStartDate, + endDate = dto.progressEndDate, + members = dto.memberCount, + maxMembers = dto.recruitCount, + daysLeft = extractDaysFromDeadline(dto.recruitEndDate), + genre = dto.category, + bookData = bookData, + recommendations = recommendations, + buttonType = determineButtonType(dto.isHost, dto.isJoining), + roomImageUrl = dto.roomImageUrl, + bookImageUrl = dto.bookImageUrl + ) + } + + private fun toGroupCardItemRoomDataFromRecommend(dto: RecommendRoomResponse): GroupCardItemRoomData { + return GroupCardItemRoomData( + id = dto.roomId, + title = dto.roomName, + participants = dto.memberCount, + maxParticipants = dto.recruitCount, + isRecruiting = true, + endDate = extractDaysFromDeadline(dto.recruitEndDate), + imageUrl = dto.roomImageUrl, + genreIndex = 0, + ) + } + + fun extractDaysFromDeadline(deadlineDate: String): Int { + return when { + deadlineDate.contains("일 뒤") -> { + deadlineDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 + } + else -> 0 + } + } + + fun toMyRoomCardData(room: MyRoomResponse): MyRoomCardData { + return MyRoomCardData( + roomId = room.roomId, + bookImageUrl = room.bookImageUrl, + roomName = room.roomName, + recruitCount = room.recruitCount, + memberCount = room.memberCount, + endDate = room.endDate, + type = room.type + ) + } + + private fun determineButtonType(isHost: Boolean, isJoining: Boolean): GroupBottomButtonType { + return when { + isHost -> GroupBottomButtonType.CLOSE + isJoining -> GroupBottomButtonType.CANCEL + else -> GroupBottomButtonType.JOIN + } + } +} \ No newline at end of file From 7bec1f8aacc359a0b1d6dcd22fad6de94ae49823 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 16:45:23 +0900 Subject: [PATCH 48/68] =?UTF-8?q?[chore]:=20PR=EB=A6=AC=EB=B7=B0=EC=9A=A9?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=EC=84=A4=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/repository/GroupRepository.kt | 267 ------------------ .../{model => }/repository/BookRepository.kt | 6 +- .../thip/data/repository/GroupRepository.kt | 175 ++++++++++++ .../data/{model => }/service/BookService.kt | 3 +- .../data/{model => }/service/GroupService.kt | 8 +- 5 files changed, 187 insertions(+), 272 deletions(-) delete mode 100644 app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt rename app/src/main/java/com/texthip/thip/data/{model => }/repository/BookRepository.kt (81%) create mode 100644 app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt rename app/src/main/java/com/texthip/thip/data/{model => }/service/BookService.kt (77%) rename app/src/main/java/com/texthip/thip/data/{model => }/service/GroupService.kt (83%) diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt deleted file mode 100644 index a002a93a..00000000 --- a/app/src/main/java/com/texthip/thip/data/model/repository/GroupRepository.kt +++ /dev/null @@ -1,267 +0,0 @@ -package com.texthip.thip.data.model.repository - -import android.content.Context -import com.texthip.thip.R -import com.texthip.thip.data.model.base.handleBaseResponse -import com.texthip.thip.data.model.group.request.CreateRoomRequest -import com.texthip.thip.data.model.group.request.RoomJoinRequest -import com.texthip.thip.data.model.group.response.PaginationResult -import com.texthip.thip.data.model.group.response.RoomMainResponse -import com.texthip.thip.data.model.service.GroupService -import com.texthip.thip.ui.group.done.mock.MyRoomCardData -import com.texthip.thip.ui.group.done.mock.MyRoomsPaginationResult -import com.texthip.thip.ui.group.myroom.mock.GroupBookData -import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.myroom.mock.GroupCardData -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class GroupRepository @Inject constructor( - private val groupService: GroupService, - @param:ApplicationContext private val context: Context -) { - private val genres = listOf( - context.getString(R.string.literature), - context.getString(R.string.science_it), - context.getString(R.string.social_science), - context.getString(R.string.humanities), - context.getString(R.string.art) - ) - - fun getGenres(): Result> { - return Result.success(genres) - } - private var cachedUserName: String? = null - - private fun mapGenreToApiCategory(genre: String): String { - return when (genre) { - "과학·IT" -> "과학/IT" - else -> genre - } - } - - fun getUserName(): Result { - val name = cachedUserName ?: "사용자" - return Result.success(name) - } - suspend fun getMyJoinedRooms(page: Int): Result> { - return try { - groupService.getJoinedRooms(page) - .handleBaseResponse() - .mapCatching { data -> - data?.let { joinedRoomsDto -> - cachedUserName = joinedRoomsDto.nickname - - val groups = joinedRoomsDto.roomList.map { dto -> - GroupCardData( - id = dto.roomId, - title = dto.bookTitle, - members = dto.memberCount, - imageUrl = dto.bookImageUrl, - progress = dto.userPercentage, - nickname = joinedRoomsDto.nickname - ) - } - - PaginationResult( - data = groups, - hasMore = !joinedRoomsDto.last, - currentPage = joinedRoomsDto.page, - nickname = joinedRoomsDto.nickname - ) - } ?: PaginationResult( - data = emptyList(), - hasMore = false, - currentPage = page, - nickname = "" - ) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getRoomSections(category: String = ""): Result> { - return try { - val finalCategory = category.ifEmpty { context.getString(R.string.literature) } - val apiCategory = mapGenreToApiCategory(finalCategory) - groupService.getRooms(apiCategory) - .handleBaseResponse() - .mapCatching { data -> - data?.let { roomsData -> - val sections = listOf( - GroupRoomSectionData( - title = context.getString(R.string.room_section_deadline), - rooms = roomsData.deadlineRoomList.map { dto -> - convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) - }, - genres = genres - ), - GroupRoomSectionData( - title = context.getString(R.string.room_section_popular), - rooms = roomsData.popularRoomList.map { dto -> - convertToGroupCardItemRoomData(dto, extractDaysFromDeadline(dto.deadlineDate)) - }, - genres = genres - ) - ) - sections - }.orEmpty() - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getMyRoomsByType(type: String?, cursor: String? = null): Result { - return try { - groupService.getMyRooms(type, cursor) - .handleBaseResponse() - .mapCatching { myRoomsDto -> - myRoomsDto?.let { data -> - val myRoomCards = data.roomList.map { room -> - MyRoomCardData( - roomId = room.roomId, - bookImageUrl = room.bookImageUrl, - roomName = room.roomName, - recruitCount = room.recruitCount, - memberCount = room.memberCount, - endDate = room.endDate, - type = room.type - ) - } - - MyRoomsPaginationResult( - data = myRoomCards, - nextCursor = data.nextCursor, - isLast = data.isLast - ) - } ?: MyRoomsPaginationResult( - data = emptyList(), - nextCursor = null, - isLast = true - ) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - private fun convertToGroupCardItemRoomData(dto: RoomMainResponse, daysLeft: Int): GroupCardItemRoomData { - return GroupCardItemRoomData( - id = dto.roomId, - title = dto.roomName, - participants = dto.memberCount, - maxParticipants = dto.recruitCount, - isRecruiting = true, - endDate = daysLeft, - imageUrl = dto.bookImageUrl, - genreIndex = 0, - isSecret = false - ) - } - - private fun extractDaysFromDeadline(deadlineDate: String): Int { - return when { - deadlineDate.contains("일 뒤") -> { - deadlineDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 - } - else -> 0 - } - } - - // TODO: 실제 검색 API 엔드포인트로 대체 필요 - suspend fun getSearchGroups(): Result> { - return Result.success(emptyList()) - } - - suspend fun getRoomRecruiting(roomId: Int): Result { - return try { - groupService.getRoomRecruiting(roomId) - .handleBaseResponse() - .mapCatching { recruitingDto -> - recruitingDto?.let { data -> - val bookData = GroupBookData( - title = data.bookTitle, - author = data.authorName, - publisher = data.publisher, - description = data.bookDescription, - imageUrl = data.bookImageUrl - ) - - val recommendations = data.recommendRooms.map { recommendDto -> - GroupCardItemRoomData( - id = recommendDto.roomId, - title = recommendDto.roomName, - participants = recommendDto.memberCount, - maxParticipants = recommendDto.recruitCount, - isRecruiting = true, - endDate = extractDaysFromDeadline(recommendDto.recruitEndDate), - imageUrl = recommendDto.roomImageUrl, - genreIndex = 0, - ) - } - - GroupRoomData( - id = data.roomId, - title = data.roomName, - isSecret = !data.isPublic, - description = data.roomDescription, - startDate = data.progressStartDate, - endDate = data.progressEndDate, - members = data.memberCount, - maxMembers = data.recruitCount, - daysLeft = extractDaysFromDeadline(data.recruitEndDate), - genre = data.category, - bookData = bookData, - recommendations = recommendations, - buttonType = determineButtonType(data.isHost, data.isJoining), - roomImageUrl = data.roomImageUrl, - bookImageUrl = data.bookImageUrl - ) - } ?: throw Exception("No recruiting data found for room $roomId") - } - } catch (e: Exception) { - Result.failure(e) - } - } - - private fun determineButtonType(isHost: Boolean, isJoining: Boolean): GroupBottomButtonType { - return when { - isHost -> GroupBottomButtonType.CLOSE - isJoining -> GroupBottomButtonType.CANCEL - else -> GroupBottomButtonType.JOIN - } - } - - suspend fun createRoom(request: CreateRoomRequest): Result { - return try { - groupService.createRoom(request) - .handleBaseResponse() - .mapCatching { createRoomResponse -> - createRoomResponse?.roomId ?: throw Exception("Failed to create room: roomId is null") - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun joinOrCancelRoom(roomId: Int, type: String): Result { - return try { - val request = RoomJoinRequest(type = type) - groupService.joinOrCancelRoom(roomId, request) - .handleBaseResponse() - .mapCatching { response -> - response?.type ?: throw Exception("Failed to join/cancel room: no response") - } - } catch (e: Exception) { - Result.failure(e) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/repository/BookRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt similarity index 81% rename from app/src/main/java/com/texthip/thip/data/model/repository/BookRepository.kt rename to app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt index f8066761..7333f420 100644 --- a/app/src/main/java/com/texthip/thip/data/model/repository/BookRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt @@ -1,8 +1,8 @@ -package com.texthip.thip.data.model.repository +package com.texthip.thip.data.repository import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.book.response.BookSavedResponse -import com.texthip.thip.data.model.service.BookService +import com.texthip.thip.data.service.BookService import javax.inject.Inject import javax.inject.Singleton @@ -11,7 +11,7 @@ class BookRepository @Inject constructor( private val bookService: BookService ) { - // 저장된/모임 책 조회 API 연동 + /** 저장된 책 또는 모임 책 목록 조회 */ suspend fun getBooks(type: String): Result> { return try { bookService.getBooks(type) diff --git a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt new file mode 100644 index 00000000..0e784b94 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt @@ -0,0 +1,175 @@ +package com.texthip.thip.data.repository + +import android.content.Context +import com.texthip.thip.R +import com.texthip.thip.data.mapper.GroupDataMapper +import com.texthip.thip.data.manager.GenreManager +import com.texthip.thip.data.manager.UserDataManager +import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.model.group.request.RoomJoinRequest +import com.texthip.thip.data.model.group.response.PaginationResult +import com.texthip.thip.data.service.GroupService +import com.texthip.thip.ui.group.done.mock.MyRoomsPaginationResult +import com.texthip.thip.ui.group.myroom.mock.GroupCardData +import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GroupRepository @Inject constructor( + private val groupService: GroupService, + private val groupDataMapper: GroupDataMapper, + private val genreManager: GenreManager, + private val userDataManager: UserDataManager, + @param:ApplicationContext private val context: Context +) { + + /** 장르 목록 조회 */ + fun getGenres(): Result> { + return Result.success(genreManager.getGenres()) + } + + /** 사용자 이름 조회(캐싱 데이터 사용)*/ + fun getUserName(): Result { + return Result.success(userDataManager.getUserName()) + } + + /** 내가 참여 중인 모임방 목록 조회 */ + suspend fun getMyJoinedRooms(page: Int): Result> { + return try { + groupService.getJoinedRooms(page) + .handleBaseResponse() + .mapCatching { data -> + data?.let { joinedRoomsDto -> + userDataManager.cacheUserName(joinedRoomsDto.nickname) + + val groups = joinedRoomsDto.roomList.map { dto -> + groupDataMapper.toGroupCardData(dto, joinedRoomsDto.nickname) + } + + PaginationResult( + data = groups, + hasMore = !joinedRoomsDto.last, + currentPage = joinedRoomsDto.page, + nickname = joinedRoomsDto.nickname + ) + } ?: PaginationResult( + data = emptyList(), + hasMore = false, + currentPage = page, + nickname = "" + ) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 카테고리별 모임방 섹션 조회 (마감임박/인기) */ + suspend fun getRoomSections(category: String = ""): Result> { + return try { + val finalCategory = category.ifEmpty { genreManager.getDefaultGenre() } + val apiCategory = genreManager.mapGenreToApiCategory(finalCategory) + + groupService.getRooms(apiCategory) + .handleBaseResponse() + .mapCatching { data -> + data?.let { roomsData -> + val sections = listOf( + GroupRoomSectionData( + title = context.getString(R.string.room_section_deadline), + rooms = roomsData.deadlineRoomList.map { dto -> + val daysLeft = groupDataMapper.extractDaysFromDeadline(dto.deadlineDate) + groupDataMapper.toGroupCardItemRoomData(dto, daysLeft) + }, + genres = genreManager.getGenres() + ), + GroupRoomSectionData( + title = context.getString(R.string.room_section_popular), + rooms = roomsData.popularRoomList.map { dto -> + val daysLeft = groupDataMapper.extractDaysFromDeadline(dto.deadlineDate) + groupDataMapper.toGroupCardItemRoomData(dto, daysLeft) + }, + genres = genreManager.getGenres() + ) + ) + sections + }.orEmpty() + } + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 타입별 내 모임방 목록 조회 */ + suspend fun getMyRoomsByType(type: String?, cursor: String? = null): Result { + return try { + groupService.getMyRooms(type, cursor) + .handleBaseResponse() + .mapCatching { myRoomsDto -> + myRoomsDto?.let { data -> + val myRoomCards = data.roomList.map { room -> + groupDataMapper.toMyRoomCardData(room) + } + + MyRoomsPaginationResult( + data = myRoomCards, + nextCursor = data.nextCursor, + isLast = data.isLast + ) + } ?: MyRoomsPaginationResult( + data = emptyList(), + nextCursor = null, + isLast = true + ) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 모집중인 모임방 상세 정보 조회 */ + suspend fun getRoomRecruiting(roomId: Int): Result { + return try { + groupService.getRoomRecruiting(roomId) + .handleBaseResponse() + .mapCatching { recruitingDto -> + recruitingDto?.let { data -> + groupDataMapper.toGroupRoomData(data) + } ?: throw Exception("No recruiting data found for room $roomId") + } + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 새 모임방 생성 */ + suspend fun createRoom(request: CreateRoomRequest): Result { + return try { + groupService.createRoom(request) + .handleBaseResponse() + .mapCatching { createRoomResponse -> + createRoomResponse?.roomId ?: throw Exception("Failed to create room: roomId is null") + } + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 모임방 참여 또는 취소 */ + suspend fun joinOrCancelRoom(roomId: Int, type: String): Result { + return try { + val request = RoomJoinRequest(type = type) + groupService.joinOrCancelRoom(roomId, request) + .handleBaseResponse() + .mapCatching { response -> + response?.type ?: throw Exception("Failed to join/cancel room: no response") + } + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt b/app/src/main/java/com/texthip/thip/data/service/BookService.kt similarity index 77% rename from app/src/main/java/com/texthip/thip/data/model/service/BookService.kt rename to app/src/main/java/com/texthip/thip/data/service/BookService.kt index c7ad24d0..e167a521 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/BookService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/BookService.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.service +package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.book.response.BookListResponse @@ -7,6 +7,7 @@ import retrofit2.http.Query interface BookService { + /** 저장된 책 또는 모임 책 목록 조회 */ @GET("books") suspend fun getBooks( @Query("type") type: String diff --git a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/service/GroupService.kt similarity index 83% rename from app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt rename to app/src/main/java/com/texthip/thip/data/service/GroupService.kt index 86e11c14..b02895c2 100644 --- a/app/src/main/java/com/texthip/thip/data/model/service/GroupService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/GroupService.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.service +package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.book.response.BookListResponse @@ -18,30 +18,36 @@ import retrofit2.http.Query interface GroupService { + /** 참여 중인 모임방 목록 조회 */ @GET("rooms/home/joined") suspend fun getJoinedRooms( @Query("page") page: Int = 1 ): BaseResponse + /** 카테고리별 모임방 목록 조회 (마감임박/인기) */ @GET("rooms") suspend fun getRooms( @Query("category") category: String = "문학" ): BaseResponse + /** 내가 만든/참여한 모임방 목록 조회 */ @GET("rooms/my") suspend fun getMyRooms( @Query("type") type: String? = null, @Query("cursor") cursor: String? = null ): BaseResponse + /** 모집중인 모임방 상세 정보 조회 */ @GET("rooms/{roomId}/recruiting") suspend fun getRoomRecruiting(@Path("roomId") roomId: Int): BaseResponse + /** 새 모임방 생성 */ @POST("rooms") suspend fun createRoom( @Body request: CreateRoomRequest ): BaseResponse + /** 모임방 참여/취소 */ @POST("rooms/{roomId}/join") suspend fun joinOrCancelRoom( @Path("roomId") roomId: Int, From f8b087a616655fbb60bc3b4e7001eda39c5837a3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 16:45:31 +0900 Subject: [PATCH 49/68] =?UTF-8?q?[chore]:=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt | 4 ++-- .../thip/ui/group/done/viewmodel/GroupDoneViewModel.kt | 2 +- .../ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt | 4 ++-- .../thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt | 2 +- .../thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt | 2 +- .../com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt | 3 +-- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt index 486ec97b..8e4731e6 100644 --- a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt +++ b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt @@ -1,7 +1,7 @@ package com.texthip.thip.data.di -import com.texthip.thip.data.model.service.BookService -import com.texthip.thip.data.model.service.GroupService +import com.texthip.thip.data.service.BookService +import com.texthip.thip.data.service.GroupService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index a1ef86f3..b026232c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -2,7 +2,7 @@ package com.texthip.thip.ui.group.done.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.done.mock.MyRoomCardData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index 39cdcd50..c7091fd5 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -3,9 +3,9 @@ package com.texthip.thip.ui.group.makeroom.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.book.response.BookSavedResponse -import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.data.model.group.request.CreateRoomRequest -import com.texthip.thip.data.model.repository.BookRepository +import com.texthip.thip.data.repository.BookRepository import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt index 9134fcd1..2112f671 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -2,7 +2,7 @@ package com.texthip.thip.ui.group.myroom.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.done.mock.MyRoomCardData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index 58df1e42..f8aefdc0 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -2,7 +2,7 @@ package com.texthip.thip.ui.group.room.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 46b7a8da..995ea01b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -2,9 +2,8 @@ package com.texthip.thip.ui.group.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.texthip.thip.data.model.repository.GroupRepository +import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupCardData -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async From c7f1077932ed724a355e936ca3d562b99842c39e Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 23:28:09 +0900 Subject: [PATCH 50/68] =?UTF-8?q?[chore]:=20Group=20=EA=B4=80=EB=A0=A8=20V?= =?UTF-8?q?iewModel=20=EB=B0=8F=20Screen=20=EC=97=90=EC=84=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8A=94=20=EC=83=81=ED=83=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/done/mock/GroupDoneUiState.kt | 10 ++++++++ .../thip/ui/group/mock/GroupUiState.kt | 20 +++++++++++++++ .../ui/group/myroom/mock/GroupMyUiState.kt | 25 +++++++++++++++++++ .../room/mock/GroupRoomRecruitUiState.kt | 18 +++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt create mode 100644 app/src/main/java/com/texthip/thip/ui/group/mock/GroupUiState.kt create mode 100644 app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt create mode 100644 app/src/main/java/com/texthip/thip/ui/group/room/mock/GroupRoomRecruitUiState.kt diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt new file mode 100644 index 00000000..6540f337 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt @@ -0,0 +1,10 @@ +package com.texthip.thip.ui.group.done.mock + +data class GroupDoneUiState( + val expiredRooms: List = emptyList(), + val isLoading: Boolean = false, + val userName: String = "", + val error: String? = null +) { + val hasContent: Boolean get() = expiredRooms.isNotEmpty() +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/mock/GroupUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/mock/GroupUiState.kt new file mode 100644 index 00000000..d4482c8b --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/mock/GroupUiState.kt @@ -0,0 +1,20 @@ +package com.texthip.thip.ui.group.mock + +import com.texthip.thip.ui.group.myroom.mock.GroupCardData +import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData + +data class GroupUiState( + val myGroups: List = emptyList(), + val hasMoreMyGroups: Boolean = true, + val isRefreshing: Boolean = false, + val isLoadingMoreMyGroups: Boolean = false, + val roomSections: List = emptyList(), + val roomSectionsError: String? = null, + val userName: String = "", + val selectedGenreIndex: Int = 0, + val showToast: Boolean = false, + val toastMessage: String = "" +) { + val hasContent: Boolean get() = myGroups.isNotEmpty() || roomSections.isNotEmpty() + val canLoadMore: Boolean get() = hasMoreMyGroups && !isRefreshing && !isLoadingMoreMyGroups +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt new file mode 100644 index 00000000..70746400 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt @@ -0,0 +1,25 @@ +package com.texthip.thip.ui.group.myroom.mock + +import com.texthip.thip.ui.group.done.mock.MyRoomCardData + +data class GroupMyUiState( + val myRooms: List = emptyList(), + val currentRoomType: String = PLAYING_AND_RECRUITING, + val isLoading: Boolean = false, + val isLoadingMore: Boolean = false, + val error: String? = null +) { + val hasContent: Boolean get() = myRooms.isNotEmpty() + val canLoadMore: Boolean get() = !isLoading && !isLoadingMore + + companion object { + const val PLAYING_AND_RECRUITING = "playingAndRecruiting" + const val RECRUITING = "recruiting" + const val PLAYING = "playing" + const val EXPIRED = "expired" + + // Room actions + const val ACTION_JOIN = "join" + const val ACTION_CANCEL = "cancel" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/mock/GroupRoomRecruitUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/room/mock/GroupRoomRecruitUiState.kt new file mode 100644 index 00000000..1cad5265 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/room/mock/GroupRoomRecruitUiState.kt @@ -0,0 +1,18 @@ +package com.texthip.thip.ui.group.room.mock + +import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType +import com.texthip.thip.ui.group.myroom.mock.GroupRoomData + +data class GroupRoomRecruitUiState( + val roomDetail: GroupRoomData? = null, + val isLoading: Boolean = false, + val currentButtonType: GroupBottomButtonType? = null, + val showToast: Boolean = false, + val toastMessage: String = "", + val showDialog: Boolean = false, + val dialogTitle: String = "", + val dialogDescription: String = "", + val shouldNavigateToGroupScreen: Boolean = false +) { + val hasRoomDetail: Boolean get() = roomDetail != null +} \ No newline at end of file From 8f05f31a65e697662cd3c5c2ecf9fcfa180cc339 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 23:29:21 +0900 Subject: [PATCH 51/68] =?UTF-8?q?[refactor]:=20=EB=B6=84=EB=A6=AC=ED=95=9C?= =?UTF-8?q?=20State=EB=A5=BC=20viewModel=EC=97=90=20=EC=A0=81=EC=9A=A9=20(?= =?UTF-8?q?#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/group/done/mock/MyRoomCardData.kt | 27 ++--- .../done/viewmodel/GroupDoneViewModel.kt | 38 ++++--- .../makeroom/mock/GroupMakeRoomUiState.kt | 6 +- .../viewmodel/GroupMakeRoomViewModel.kt | 73 ++++++------ .../myroom/viewmodel/GroupMyViewModel.kt | 52 ++++----- .../viewmodel/GroupRoomRecruitViewModel.kt | 106 +++++++++--------- .../thip/ui/group/viewmodel/GroupViewModel.kt | 95 +++++++--------- 7 files changed, 189 insertions(+), 208 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt index 1fc951bd..005e31f7 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -1,13 +1,16 @@ package com.texthip.thip.ui.group.done.mock +import com.texthip.thip.data.mapper.GroupDataMapper +import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState + data class MyRoomCardData( val roomId: Int, - val bookImageUrl: String?, // API에서 받은 이미지 URL + val bookImageUrl: String?, val roomName: String, val recruitCount: Int, val memberCount: Int, val endDate: String, - val type: String // "playingAndRecruiting", "recruiting", "playing", "expired" + val type: String ) data class MyRoomsPaginationResult( @@ -16,23 +19,17 @@ data class MyRoomsPaginationResult( val isLast: Boolean ) -// MyRoomCardData를 위한 타입 기반 모집 상태 확인 함수 +// 타입 기반 모집 상태 확인 함수 fun MyRoomCardData.isRecruitingByType(): Boolean { return when (type) { - "recruiting" -> true - "playingAndRecruiting" -> false - "playing" -> false - "expired" -> false - else -> false // 타입 정보가 없으면 기본값 + GroupMyUiState.RECRUITING -> true + GroupMyUiState.PLAYING_AND_RECRUITING -> false + GroupMyUiState.PLAYING -> false + GroupMyUiState.EXPIRED -> false + else -> false } } -// endDate 문자열을 일수로 변환하는 함수 (예: "25일 뒤" -> 25) fun MyRoomCardData.getEndDateInDays(): Int { - return when { - endDate.contains("일 뒤") -> { - endDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 - } - else -> 0 // 파싱할 수 없는 경우 0 반환 - } + return GroupDataMapper().extractDaysFromDeadline(endDate) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index b026232c..7edd3db1 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -3,7 +3,8 @@ package com.texthip.thip.ui.group.done.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import com.texthip.thip.ui.group.done.mock.GroupDoneUiState +import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,18 +17,16 @@ class GroupDoneViewModel @Inject constructor( private val repository: GroupRepository ) : ViewModel() { - private val _expiredRooms = MutableStateFlow>(emptyList()) - val expiredRooms: StateFlow> = _expiredRooms.asStateFlow() - - private val _isLoading = MutableStateFlow(false) - val isLoading: StateFlow = _isLoading.asStateFlow() - - private val _userName = MutableStateFlow("") - val userName: StateFlow = _userName.asStateFlow() + private val _uiState = MutableStateFlow(GroupDoneUiState()) + val uiState: StateFlow = _uiState.asStateFlow() private var nextCursor: String? = null private var isLastPage = false private var isLoadingMore = false + + private fun updateState(update: (GroupDoneUiState) -> GroupDoneUiState) { + _uiState.value = update(_uiState.value) + } init { loadInitialData() @@ -42,7 +41,7 @@ class GroupDoneViewModel @Inject constructor( viewModelScope.launch { repository.getUserName() .onSuccess { name -> - _userName.value = name + updateState { it.copy(userName = name) } } } } @@ -54,25 +53,30 @@ class GroupDoneViewModel @Inject constructor( viewModelScope.launch { try { if (reset) { - _isLoading.value = true + updateState { it.copy(isLoading = true, expiredRooms = emptyList()) } nextCursor = null isLastPage = false - _expiredRooms.value = emptyList() } else { isLoadingMore = true } - repository.getMyRoomsByType("expired", nextCursor) + repository.getMyRoomsByType(GroupMyUiState.EXPIRED, nextCursor) .onSuccess { paginationResult -> - val currentList = if (reset) emptyList() else _expiredRooms.value - _expiredRooms.value = currentList + paginationResult.data + val currentList = if (reset) emptyList() else uiState.value.expiredRooms + updateState { + it.copy( + expiredRooms = currentList + paginationResult.data, + error = null + ) + } nextCursor = paginationResult.nextCursor isLastPage = paginationResult.isLast } - .onFailure { + .onFailure { exception -> + updateState { it.copy(error = exception.message) } } } finally { - _isLoading.value = false + updateState { it.copy(isLoading = false) } isLoadingMore = false } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt index 7143a1d2..217c9187 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt @@ -15,7 +15,11 @@ data class GroupMakeRoomUiState( val isPrivate: Boolean = false, val password: String = "", val isLoading: Boolean = false, - val errorMessage: String? = null + val errorMessage: String? = null, + val savedBooks: List = emptyList(), + val groupBooks: List = emptyList(), + val isLoadingBooks: Boolean = false, + val genres: List = emptyList() ) { // 유효성 검사 로직 val isDurationValid: Boolean diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index c7091fd5..fe8f5329 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -25,18 +25,10 @@ class GroupMakeRoomViewModel @Inject constructor( private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) val uiState: StateFlow = _uiState.asStateFlow() - - private val _savedBooks = MutableStateFlow>(emptyList()) - val savedBooks: StateFlow> = _savedBooks.asStateFlow() - - private val _groupBooks = MutableStateFlow>(emptyList()) - val groupBooks: StateFlow> = _groupBooks.asStateFlow() - private val _isLoadingBooks = MutableStateFlow(false) - val isLoadingBooks: StateFlow = _isLoadingBooks.asStateFlow() - - private val _genres = MutableStateFlow>(emptyList()) - val genres: StateFlow> = _genres.asStateFlow() + private fun updateState(update: (GroupMakeRoomUiState) -> GroupMakeRoomUiState) { + _uiState.value = update(_uiState.value) + } init { loadGenres() @@ -46,17 +38,17 @@ class GroupMakeRoomViewModel @Inject constructor( viewModelScope.launch { groupRepository.getGenres() .onSuccess { genresList -> - _genres.value = genresList + updateState { it.copy(genres = genresList) } } } } fun selectBook(book: BookData) { - _uiState.value = _uiState.value.copy(selectedBook = book) + updateState { it.copy(selectedBook = book) } } fun toggleBookSearchSheet(show: Boolean) { - _uiState.value = _uiState.value.copy(showBookSearchSheet = show) + updateState { it.copy(showBookSearchSheet = show) } if (show) { loadBooks() } @@ -64,26 +56,25 @@ class GroupMakeRoomViewModel @Inject constructor( private fun loadBooks() { viewModelScope.launch { - _isLoadingBooks.value = true + updateState { it.copy(isLoadingBooks = true) } try { val savedBooksResult = bookRepository.getBooks("saved") savedBooksResult.onSuccess { bookDtos -> - _savedBooks.value = bookDtos.map { it.toBookData() } + updateState { it.copy(savedBooks = bookDtos.map { dto -> dto.toBookData() }) } }.onFailure { - _savedBooks.value = emptyList() + updateState { it.copy(savedBooks = emptyList()) } } val groupBooksResult = bookRepository.getBooks("joining") groupBooksResult.onSuccess { bookDtos -> - _groupBooks.value = bookDtos.map { it.toBookData() } + updateState { it.copy(groupBooks = bookDtos.map { dto -> dto.toBookData() }) } }.onFailure { - _groupBooks.value = emptyList() + updateState { it.copy(groupBooks = emptyList()) } } } catch (e: Exception) { - _savedBooks.value = emptyList() - _groupBooks.value = emptyList() + updateState { it.copy(savedBooks = emptyList(), groupBooks = emptyList()) } } finally { - _isLoadingBooks.value = false + updateState { it.copy(isLoadingBooks = false) } } } } @@ -98,37 +89,41 @@ class GroupMakeRoomViewModel @Inject constructor( } fun selectGenre(index: Int) { - _uiState.value = _uiState.value.copy(selectedGenreIndex = index) + updateState { it.copy(selectedGenreIndex = index) } } fun updateRoomTitle(title: String) { - _uiState.value = _uiState.value.copy(roomTitle = title) + updateState { it.copy(roomTitle = title) } } fun updateRoomDescription(description: String) { - _uiState.value = _uiState.value.copy(roomDescription = description) + updateState { it.copy(roomDescription = description) } } fun setDateRange(startDate: LocalDate, endDate: LocalDate) { - _uiState.value = _uiState.value.copy( - meetingStartDate = startDate, - meetingEndDate = endDate - ) + updateState { + it.copy( + meetingStartDate = startDate, + meetingEndDate = endDate + ) + } } fun setMemberLimit(count: Int) { - _uiState.value = _uiState.value.copy(memberLimit = count) + updateState { it.copy(memberLimit = count) } } fun togglePrivate(isPrivate: Boolean) { - _uiState.value = _uiState.value.copy( - isPrivate = isPrivate, - password = if (!isPrivate) "" else _uiState.value.password - ) + updateState { + it.copy( + isPrivate = isPrivate, + password = if (!isPrivate) "" else it.password + ) + } } fun updatePassword(password: String) { - _uiState.value = _uiState.value.copy(password = password) + updateState { it.copy(password = password) } } fun createGroup(onSuccess: (Int) -> Unit, onError: (String) -> Unit) { @@ -147,7 +142,7 @@ class GroupMakeRoomViewModel @Inject constructor( viewModelScope.launch { try { - _uiState.value = currentState.copy(isLoading = true, errorMessage = null) + updateState { it.copy(isLoading = true, errorMessage = null) } val request = CreateRoomRequest( isbn = selectedBook.isbn, @@ -170,13 +165,13 @@ class GroupMakeRoomViewModel @Inject constructor( } catch (e: Exception) { onError("네트워크 오류가 발생했습니다: ${e.message}") } finally { - _uiState.value = _uiState.value.copy(isLoading = false) + updateState { it.copy(isLoading = false) } } } } private fun getApiCategoryName(genreIndex: Int): String { - val currentGenres = _genres.value + val currentGenres = uiState.value.genres if (genreIndex >= 0 && genreIndex < currentGenres.size) { val genre = currentGenres[genreIndex] return when (genre) { @@ -188,6 +183,6 @@ class GroupMakeRoomViewModel @Inject constructor( } fun clearError() { - _uiState.value = _uiState.value.copy(errorMessage = null) + updateState { it.copy(errorMessage = null) } } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt index 2112f671..320af562 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -3,7 +3,7 @@ package com.texthip.thip.ui.group.myroom.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,21 +16,16 @@ class GroupMyViewModel @Inject constructor( private val repository: GroupRepository ) : ViewModel() { - private val _myRooms = MutableStateFlow>(emptyList()) - val myRooms: StateFlow> = _myRooms.asStateFlow() - - private val _isLoading = MutableStateFlow(false) - val isLoading: StateFlow = _isLoading.asStateFlow() - - private val _isLoadingMore = MutableStateFlow(false) - val isLoadingMore: StateFlow = _isLoadingMore.asStateFlow() - - private val _currentRoomType = MutableStateFlow("playingAndRecruiting") - val currentRoomType: StateFlow = _currentRoomType.asStateFlow() + private val _uiState = MutableStateFlow(GroupMyUiState()) + val uiState: StateFlow = _uiState.asStateFlow() private var nextCursor: String? = null private var isLastPage = false private var isLoadingData = false + + private fun updateState(update: (GroupMyUiState) -> GroupMyUiState) { + _uiState.value = update(_uiState.value) + } init { loadMyRooms(reset = true) @@ -45,27 +40,31 @@ class GroupMyViewModel @Inject constructor( isLoadingData = true if (reset) { - _isLoading.value = true + updateState { it.copy(isLoading = true, myRooms = emptyList()) } nextCursor = null isLastPage = false - _myRooms.value = emptyList() } else { - _isLoadingMore.value = true + updateState { it.copy(isLoadingMore = true) } } - repository.getMyRoomsByType(_currentRoomType.value, nextCursor) + repository.getMyRoomsByType(uiState.value.currentRoomType, nextCursor) .onSuccess { paginationResult -> - val currentList = if (reset) emptyList() else _myRooms.value - _myRooms.value = currentList + paginationResult.data + val currentList = if (reset) emptyList() else uiState.value.myRooms + updateState { + it.copy( + myRooms = currentList + paginationResult.data, + error = null + ) + } nextCursor = paginationResult.nextCursor isLastPage = paginationResult.isLast } - .onFailure { + .onFailure { exception -> + updateState { it.copy(error = exception.message) } } } finally { isLoadingData = false - _isLoading.value = false - _isLoadingMore.value = false + updateState { it.copy(isLoading = false, isLoadingMore = false) } } } } @@ -79,16 +78,9 @@ class GroupMyViewModel @Inject constructor( } fun changeRoomType(roomType: String) { - if (roomType != _currentRoomType.value) { - _currentRoomType.value = roomType + if (roomType != uiState.value.currentRoomType) { + updateState { it.copy(currentRoomType = roomType) } loadMyRooms(reset = true) } } - - // Room type - companion object { - const val PLAYING_AND_RECRUITING = "playingAndRecruiting" - const val RECRUITING = "recruiting" - const val PLAYING = "playing" - } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index f8aefdc0..08dc37f7 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -4,7 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState +import com.texthip.thip.ui.group.room.mock.GroupRoomRecruitUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -17,58 +18,42 @@ class GroupRoomRecruitViewModel @Inject constructor( private val repository: GroupRepository ) : ViewModel() { - private val _roomDetail = MutableStateFlow(null) - val roomDetail: StateFlow = _roomDetail.asStateFlow() - - private val _isLoading = MutableStateFlow(false) - val isLoading: StateFlow = _isLoading.asStateFlow() - - private val _currentButtonType = MutableStateFlow(null) - val currentButtonType: StateFlow = _currentButtonType.asStateFlow() - - private val _showToast = MutableStateFlow(false) - val showToast: StateFlow = _showToast.asStateFlow() - - private val _toastMessage = MutableStateFlow("") - val toastMessage: StateFlow = _toastMessage.asStateFlow() - - private val _showDialog = MutableStateFlow(false) - val showDialog: StateFlow = _showDialog.asStateFlow() - - private val _dialogTitle = MutableStateFlow("") - val dialogTitle: StateFlow = _dialogTitle.asStateFlow() - - private val _dialogDescription = MutableStateFlow("") - val dialogDescription: StateFlow = _dialogDescription.asStateFlow() - - private val _shouldNavigateToGroupScreen = MutableStateFlow(false) - val shouldNavigateToGroupScreen: StateFlow = _shouldNavigateToGroupScreen.asStateFlow() + private val _uiState = MutableStateFlow(GroupRoomRecruitUiState()) + val uiState: StateFlow = _uiState.asStateFlow() private var pendingAction: (() -> Unit)? = null + + private fun updateState(update: (GroupRoomRecruitUiState) -> GroupRoomRecruitUiState) { + _uiState.value = update(_uiState.value) + } fun loadRoomDetail(roomId: Int) { viewModelScope.launch { - _isLoading.value = true + updateState { it.copy(isLoading = true) } repository.getRoomRecruiting(roomId) .onSuccess { data -> - _roomDetail.value = data - _currentButtonType.value = data.buttonType + updateState { + it.copy( + roomDetail = data, + currentButtonType = data.buttonType, + isLoading = false + ) + } } .onFailure { error -> + updateState { it.copy(isLoading = false) } } - - _isLoading.value = false } } fun onParticipationClick() { viewModelScope.launch { - val roomId = _roomDetail.value?.id ?: return@launch + val roomId = uiState.value.roomDetail?.id ?: return@launch - repository.joinOrCancelRoom(roomId, "join") + repository.joinOrCancelRoom(roomId, GroupMyUiState.ACTION_JOIN) .onSuccess { - _currentButtonType.value = GroupBottomButtonType.CANCEL + updateState { it.copy(currentButtonType = GroupBottomButtonType.CANCEL) } showToastMessage("참여가 완료되었습니다") } .onFailure { error -> @@ -78,59 +63,76 @@ class GroupRoomRecruitViewModel @Inject constructor( } fun onCancelParticipationClick(dialogTitle: String, dialogDescription: String) { - _dialogTitle.value = dialogTitle - _dialogDescription.value = dialogDescription + updateState { + it.copy( + dialogTitle = dialogTitle, + dialogDescription = dialogDescription, + showDialog = true + ) + } pendingAction = { viewModelScope.launch { - val roomId = _roomDetail.value?.id ?: return@launch + val roomId = uiState.value.roomDetail?.id ?: return@launch - repository.joinOrCancelRoom(roomId, "cancel") + repository.joinOrCancelRoom(roomId, GroupMyUiState.ACTION_CANCEL) .onSuccess { - _currentButtonType.value = GroupBottomButtonType.JOIN - _toastMessage.value = "모임방 참여가 취소되었어요! 다른 방을 찾아보세요." - _shouldNavigateToGroupScreen.value = true + updateState { + it.copy( + currentButtonType = GroupBottomButtonType.JOIN, + toastMessage = "모임방 참여가 취소되었어요! 다른 방을 찾아보세요.", + showToast = true, + shouldNavigateToGroupScreen = true + ) + } } .onFailure { error -> showToastMessage("참여 취소 중 오류가 발생했습니다: ${error.message}") } } } - _showDialog.value = true } fun onCloseRecruitmentClick(dialogTitle: String, dialogDescription: String) { - _dialogTitle.value = dialogTitle - _dialogDescription.value = dialogDescription + updateState { + it.copy( + dialogTitle = dialogTitle, + dialogDescription = dialogDescription, + showDialog = true + ) + } pendingAction = { viewModelScope.launch { // TODO: 실제 모집 마감 API 호출 showToastMessage("모집이 마감되었습니다") } } - _showDialog.value = true } fun onDialogConfirm() { - _showDialog.value = false + updateState { it.copy(showDialog = false) } pendingAction?.invoke() pendingAction = null } fun onDialogCancel() { - _showDialog.value = false + updateState { it.copy(showDialog = false) } pendingAction = null } fun hideToast() { - _showToast.value = false + updateState { it.copy(showToast = false) } } fun onNavigatedToGroupScreen() { - _shouldNavigateToGroupScreen.value = false + updateState { it.copy(shouldNavigateToGroupScreen = false) } } private fun showToastMessage(message: String) { - _toastMessage.value = message - _showToast.value = true + updateState { + it.copy( + toastMessage = message, + showToast = true + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 995ea01b..b41df540 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -3,8 +3,7 @@ package com.texthip.thip.ui.group.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.myroom.mock.GroupCardData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData +import com.texthip.thip.ui.group.mock.GroupUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -19,42 +18,18 @@ class GroupViewModel @Inject constructor( private val repository: GroupRepository ) : ViewModel() { - private val _myGroups = MutableStateFlow>(emptyList()) - val myGroups: StateFlow> = _myGroups - - private val _hasMoreMyGroups = MutableStateFlow(true) - val hasMoreMyGroups: StateFlow = _hasMoreMyGroups.asStateFlow() - - private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow = _isRefreshing.asStateFlow() - - private val _isLoadingMoreMyGroups = MutableStateFlow(false) - val isLoadingMoreMyGroups: StateFlow = _isLoadingMoreMyGroups.asStateFlow() + private val _uiState = MutableStateFlow(GroupUiState()) + val uiState: StateFlow = _uiState.asStateFlow() private var currentMyGroupsPage = 1 private var loadedPagesCount = 0 private val pagesPerBatch = 3 private val preloadThreshold = 2 private var isBatchLoading = false - - private val _roomSections = MutableStateFlow>(emptyList()) - val roomSections: StateFlow> = _roomSections.asStateFlow() - - private val _roomSectionsError = MutableStateFlow(null) - val roomSectionsError: StateFlow = _roomSectionsError.asStateFlow() - - private val _userName = MutableStateFlow("") - val userName: StateFlow = _userName.asStateFlow() - - - private val _selectedGenreIndex = MutableStateFlow(0) - val selectedGenreIndex: StateFlow = _selectedGenreIndex.asStateFlow() - - private val _showToast = MutableStateFlow(false) - val showToast: StateFlow = _showToast.asStateFlow() - private val _toastMessage = MutableStateFlow("") - val toastMessage: StateFlow = _toastMessage.asStateFlow() + private fun updateState(update: (GroupUiState) -> GroupUiState) { + _uiState.value = update(_uiState.value) + } init { loadInitialData() @@ -70,7 +45,7 @@ class GroupViewModel @Inject constructor( viewModelScope.launch { repository.getUserName() .onSuccess { userName -> - _userName.value = userName + updateState { it.copy(userName = userName) } } } } @@ -78,32 +53,36 @@ class GroupViewModel @Inject constructor( fun loadMyGroups(reset: Boolean = false) = viewModelScope.launch { if (reset) { resetMyGroupsData() - _isRefreshing.value = true + updateState { it.copy(isRefreshing = true) } } try { loadPageBatchSuspend() } finally { - _isRefreshing.value = false + updateState { it.copy(isRefreshing = false) } } } private suspend fun loadPageBatchSuspend() { - if (!_hasMoreMyGroups.value || isBatchLoading) return + if (!uiState.value.hasMoreMyGroups || isBatchLoading) return try { isBatchLoading = true - _isLoadingMoreMyGroups.value = true + updateState { it.copy(isLoadingMoreMyGroups = true) } val currentBatchStart = currentMyGroupsPage val batchEndPage = currentBatchStart + pagesPerBatch - 1 for (page in currentBatchStart..batchEndPage) { - if (!_hasMoreMyGroups.value) break + if (!uiState.value.hasMoreMyGroups) break repository.getMyJoinedRooms(page) .onSuccess { paginationResult -> - _myGroups.value = _myGroups.value + paginationResult.data - _hasMoreMyGroups.value = paginationResult.hasMore + updateState { + it.copy( + myGroups = it.myGroups + paginationResult.data, + hasMoreMyGroups = paginationResult.hasMore + ) + } loadedPagesCount++ currentMyGroupsPage = page + 1 } @@ -113,7 +92,7 @@ class GroupViewModel @Inject constructor( } } finally { isBatchLoading = false - _isLoadingMoreMyGroups.value = false + updateState { it.copy(isLoadingMoreMyGroups = false) } } } @@ -125,7 +104,7 @@ class GroupViewModel @Inject constructor( val currentPageEquivalent = (cardIndex / 3) + 1 if (currentPageEquivalent >= loadedPagesCount - preloadThreshold && - _hasMoreMyGroups.value && !isBatchLoading + uiState.value.hasMoreMyGroups && !isBatchLoading ) { loadPageBatch() } @@ -133,10 +112,10 @@ class GroupViewModel @Inject constructor( private fun loadRoomSections() { viewModelScope.launch { - _roomSectionsError.value = null + updateState { it.copy(roomSectionsError = null) } val genresResult = repository.getGenres() - val selectedIndex = _selectedGenreIndex.value + val selectedIndex = uiState.value.selectedGenreIndex val selectedGenre = if (genresResult.isSuccess) { val genres = genresResult.getOrThrow() if (selectedIndex >= 0 && selectedIndex < genres.size) { @@ -150,10 +129,10 @@ class GroupViewModel @Inject constructor( repository.getRoomSections(selectedGenre) .onSuccess { sections -> - _roomSections.value = sections + updateState { it.copy(roomSections = sections) } } .onFailure { error -> - _roomSectionsError.value = error.message + updateState { it.copy(roomSectionsError = error.message) } } } } @@ -162,8 +141,8 @@ class GroupViewModel @Inject constructor( val genresResult = repository.getGenres() if (genresResult.isSuccess) { val genres = genresResult.getOrThrow() - if (genreIndex >= 0 && genreIndex < genres.size && genreIndex != _selectedGenreIndex.value) { - _selectedGenreIndex.value = genreIndex + if (genreIndex >= 0 && genreIndex < genres.size && genreIndex != uiState.value.selectedGenreIndex) { + updateState { it.copy(selectedGenreIndex = genreIndex) } loadRoomSections() } } @@ -173,7 +152,7 @@ class GroupViewModel @Inject constructor( fun refreshGroupData() { viewModelScope.launch { - _isRefreshing.value = true + updateState { it.copy(isRefreshing = true) } try { val jobs = listOf( async { loadUserName() }, @@ -186,7 +165,7 @@ class GroupViewModel @Inject constructor( jobs.awaitAll() } finally { - _isRefreshing.value = false + updateState { it.copy(isRefreshing = false) } } } } @@ -194,17 +173,25 @@ class GroupViewModel @Inject constructor( private fun resetMyGroupsData() { currentMyGroupsPage = 1 loadedPagesCount = 0 - _myGroups.value = emptyList() - _hasMoreMyGroups.value = true + updateState { + it.copy( + myGroups = emptyList(), + hasMoreMyGroups = true + ) + } } fun showToastMessage(message: String) { - _toastMessage.value = message - _showToast.value = true + updateState { + it.copy( + toastMessage = message, + showToast = true + ) + } } fun hideToast() { - _showToast.value = false + updateState { it.copy(showToast = false) } } fun refreshDataOnScreenEnter() { From 41246e7e6712213f5f04a2b3af1785cbc7df6ea3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Thu, 7 Aug 2025 23:30:06 +0900 Subject: [PATCH 52/68] =?UTF-8?q?[refactor]:=20=EB=B6=84=EB=A6=AC=ED=95=9C?= =?UTF-8?q?=20State=EB=A5=BC=20Screen=20=EB=B0=8F=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=EC=97=90=20=EC=A0=81=EC=9A=A9=20(#6?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/done/Screen/GroupDoneScreen.kt | 12 +++--- .../makeroom/screen/GroupMakeRoomScreen.kt | 12 ++---- .../ui/group/myroom/screen/GroupMyScreen.kt | 38 ++++++++-------- .../room/screen/GroupRoomRecruitScreen.kt | 43 ++++++++----------- .../thip/ui/group/screen/GroupScreen.kt | 26 +++++------ .../navigator/navigations/GroupNavigation.kt | 7 +-- 6 files changed, 58 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt index e6eb55cf..414285e6 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt @@ -38,9 +38,7 @@ fun GroupDoneScreen( onNavigateBack: () -> Unit = {}, viewModel: GroupDoneViewModel = hiltViewModel() ) { - val expiredRooms by viewModel.expiredRooms.collectAsState() - val isLoading by viewModel.isLoading.collectAsState() - val userName by viewModel.userName.collectAsState() + val uiState by viewModel.uiState.collectAsState() val listState = rememberLazyListState() // 무한 스크롤을 위한 로직 @@ -53,7 +51,7 @@ fun GroupDoneScreen( } LaunchedEffect(shouldLoadMore) { - if (shouldLoadMore && expiredRooms.isNotEmpty()) { + if (shouldLoadMore && uiState.expiredRooms.isNotEmpty()) { viewModel.loadMoreExpiredRooms() } } @@ -67,7 +65,7 @@ fun GroupDoneScreen( ) PullToRefreshBox( - isRefreshing = isLoading, + isRefreshing = uiState.isLoading, onRefresh = { viewModel.refreshData() }, modifier = Modifier.fillMaxSize() ) { @@ -87,13 +85,13 @@ fun GroupDoneScreen( ) { item { Text( - text = stringResource(R.string.group_done_user_comment, userName), + text = stringResource(R.string.group_done_user_comment, uiState.userName), color = colors.White, style = typography.menu_r400_s14_h24 ) } - items(expiredRooms) { room -> + items(uiState.expiredRooms) { room -> CardItemRoom( title = room.roomName, imageUrl = room.bookImageUrl, diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt index 91bb814b..6c49dc8f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt @@ -51,11 +51,7 @@ fun GroupMakeRoomScreen( modifier: Modifier = Modifier ) { val uiState by viewModel.uiState.collectAsState() - val savedBooks by viewModel.savedBooks.collectAsState() - val groupBooks by viewModel.groupBooks.collectAsState() - val isLoadingBooks by viewModel.isLoadingBooks.collectAsState() val scrollState = rememberScrollState() - val genres by viewModel.genres.collectAsState() // 에러 메시지 표시 LaunchedEffect(uiState.errorMessage) { @@ -115,7 +111,7 @@ fun GroupMakeRoomScreen( Spacer(modifier = Modifier.padding(top = 12.dp)) GenreChipRow( modifier = Modifier.width(18.dp), - genres = genres, + genres = uiState.genres, selectedIndex = uiState.selectedGenreIndex, onSelect = viewModel::selectGenre ) @@ -219,9 +215,9 @@ fun GroupMakeRoomScreen( onRequestBook = { viewModel.toggleBookSearchSheet(false) }, - savedBooks = savedBooks, - groupBooks = groupBooks, - isLoading = isLoadingBooks + savedBooks = uiState.savedBooks, + groupBooks = uiState.groupBooks, + isLoading = uiState.isLoadingBooks ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index 3b15838f..fc0882ec 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -33,6 +33,7 @@ import com.texthip.thip.ui.group.done.mock.isRecruitingByType import com.texthip.thip.ui.group.done.mock.getEndDateInDays import com.texthip.thip.ui.group.myroom.component.GroupMyRoomFilterRow import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel +import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -43,9 +44,7 @@ fun GroupMyScreen( onNavigateBack: () -> Unit = {}, viewModel: GroupMyViewModel = hiltViewModel() ) { - val myRooms by viewModel.myRooms.collectAsState() - val isLoading by viewModel.isLoading.collectAsState() - val currentRoomType by viewModel.currentRoomType.collectAsState() + val uiState by viewModel.uiState.collectAsState() val listState = rememberLazyListState() // 무한 스크롤 로직 @@ -58,23 +57,22 @@ fun GroupMyScreen( } LaunchedEffect(shouldLoadMore) { - if (shouldLoadMore && myRooms.isNotEmpty()) { + if (shouldLoadMore && uiState.myRooms.isNotEmpty()) { viewModel.loadMoreMyRooms() } } - // Filter 상태를 room type에 따라 설정 (토글 방식) - val selectedStates = remember(currentRoomType) { - when (currentRoomType) { - GroupMyViewModel.PLAYING -> booleanArrayOf(true, false) - GroupMyViewModel.RECRUITING -> booleanArrayOf(false, true) - else -> booleanArrayOf(false, false) // playingAndRecruiting (둘 다 비활성화 = 전체) + // Filter 상태를 + val selectedStates = remember(uiState.currentRoomType) { + when (uiState.currentRoomType) { + GroupMyUiState.PLAYING -> booleanArrayOf(true, false) + GroupMyUiState.RECRUITING -> booleanArrayOf(false, true) + else -> booleanArrayOf(false, false) // playingAndRecruiting } } Column( Modifier - .background(colors.Black) .fillMaxSize() ) { DefaultTopAppBar( @@ -83,7 +81,7 @@ fun GroupMyScreen( ) PullToRefreshBox( - isRefreshing = isLoading, + isRefreshing = uiState.isLoading, onRefresh = { viewModel.refreshData() }, modifier = Modifier.fillMaxSize() ) { @@ -103,23 +101,23 @@ fun GroupMyScreen( idx == 0 -> { if (selectedStates[0]) { // 이미 선택된 상태면 전체로 변경 - GroupMyViewModel.PLAYING_AND_RECRUITING + GroupMyUiState.PLAYING_AND_RECRUITING } else { // 선택되지 않은 상태면 진행중만 - GroupMyViewModel.PLAYING + GroupMyUiState.PLAYING } } // 모집중 버튼을 눌렀을 때 idx == 1 -> { if (selectedStates[1]) { // 이미 선택된 상태면 전체로 변경 - GroupMyViewModel.PLAYING_AND_RECRUITING + GroupMyUiState.PLAYING_AND_RECRUITING } else { // 선택되지 않은 상태면 모집중만 - GroupMyViewModel.RECRUITING + GroupMyUiState.RECRUITING } } - else -> GroupMyViewModel.PLAYING_AND_RECRUITING + else -> GroupMyUiState.PLAYING_AND_RECRUITING } viewModel.changeRoomType(newRoomType) } @@ -127,14 +125,14 @@ fun GroupMyScreen( Spacer(modifier = Modifier.height(20.dp)) - if (myRooms.isNotEmpty()) { + if (uiState.myRooms.isNotEmpty()) { LazyColumn( state = listState, verticalArrangement = Arrangement.spacedBy(20.dp), contentPadding = PaddingValues(bottom = 20.dp), modifier = Modifier.fillMaxSize() ) { - items(myRooms) { room -> + items(uiState.myRooms) { room -> CardItemRoom( title = room.roomName, participants = room.memberCount, @@ -146,7 +144,7 @@ fun GroupMyScreen( ) } } - } else if (!isLoading) { + } else if (!uiState.isLoading) { Column( modifier = Modifier .fillMaxSize(), diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index 10942ebf..1fc783b1 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -62,35 +62,26 @@ fun GroupRoomRecruitScreen( ) { val context = LocalContext.current - // ViewModel states - val roomDetail by viewModel.roomDetail.collectAsState() - val isLoading by viewModel.isLoading.collectAsState() - val currentButtonType by viewModel.currentButtonType.collectAsState() - val showToast by viewModel.showToast.collectAsState() - val toastMessage by viewModel.toastMessage.collectAsState() - val showDialog by viewModel.showDialog.collectAsState() - val dialogTitle by viewModel.dialogTitle.collectAsState() - val dialogDescription by viewModel.dialogDescription.collectAsState() - val shouldNavigateToGroupScreen by viewModel.shouldNavigateToGroupScreen.collectAsState() + val uiState by viewModel.uiState.collectAsState() - // 데이터 로딩 - mockRoomDetail이 없을 때만 실행 + // 데이터 로딩 LaunchedEffect(roomId) { if (mockRoomDetail == null) { viewModel.loadRoomDetail(roomId) } } - // GroupScreen으로 네비게이션 처리 - LaunchedEffect(shouldNavigateToGroupScreen) { - if (shouldNavigateToGroupScreen) { - onNavigateToGroupScreen(toastMessage) // 토스트 메시지와 함께 네비게이션 + // GroupScreen으로 네비게이션 + LaunchedEffect(uiState.shouldNavigateToGroupScreen, uiState.toastMessage) { + if (uiState.shouldNavigateToGroupScreen) { + onNavigateToGroupScreen(uiState.toastMessage) viewModel.onNavigatedToGroupScreen() } } Box(Modifier.fillMaxSize()) { // 로딩 상태 - if (isLoading) { + if (uiState.isLoading) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -101,7 +92,7 @@ fun GroupRoomRecruitScreen( } // 데이터가 없는 경우 (Preview에서는 mock 데이터 사용) - val detail = roomDetail ?: mockRoomDetail ?: return@Box + val detail = uiState.roomDetail ?: mockRoomDetail ?: return@Box Image( painter = painterResource(id = R.drawable.group_room_recruiting), @@ -360,7 +351,7 @@ fun GroupRoomRecruitScreen( } // 하단 버튼 (Preview에서는 mockRoomDetail의 buttonType 사용) - val buttonType = currentButtonType ?: mockRoomDetail?.buttonType + val buttonType = uiState.currentButtonType ?: mockRoomDetail?.buttonType if (buttonType != null) { val buttonText = when (buttonType) { GroupBottomButtonType.JOIN -> stringResource(R.string.group_room_screen_participant) @@ -407,10 +398,10 @@ fun GroupRoomRecruitScreen( } } - // 토스트 팝업 - GroupScreen으로 네비게이션할 때는 표시하지 않음 - if (showToast && !shouldNavigateToGroupScreen) { + // 토스트 팝업 + if (uiState.showToast && !uiState.shouldNavigateToGroupScreen) { ToastWithDate( - message = toastMessage, + message = uiState.toastMessage, modifier = Modifier .align(Alignment.TopCenter) .padding(horizontal = 20.dp, vertical = 16.dp) @@ -418,7 +409,7 @@ fun GroupRoomRecruitScreen( ) } - if (showDialog) { + if (uiState.showDialog) { Box( modifier = Modifier .fillMaxSize() @@ -427,8 +418,8 @@ fun GroupRoomRecruitScreen( contentAlignment = Alignment.Center ) { DialogPopup( - title = dialogTitle, - description = dialogDescription, + title = uiState.dialogTitle, + description = uiState.dialogDescription, onConfirm = { viewModel.onDialogConfirm() }, @@ -441,8 +432,8 @@ fun GroupRoomRecruitScreen( } // 토스트 3초 후 자동 숨김 (GroupScreen으로 네비게이션 시에는 GroupScreen에서 관리) - LaunchedEffect(showToast, shouldNavigateToGroupScreen) { - if (showToast && !shouldNavigateToGroupScreen) { + LaunchedEffect(uiState.showToast, uiState.shouldNavigateToGroupScreen) { + if (uiState.showToast && !uiState.shouldNavigateToGroupScreen) { delay(3000) viewModel.hideToast() } diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index ac0953d8..6f5a3567 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -52,20 +52,14 @@ fun GroupScreen( LaunchedEffect(Unit) { viewModel.refreshDataOnScreenEnter() } - val myGroups by viewModel.myGroups.collectAsState() - val roomSections by viewModel.roomSections.collectAsState() - val selectedGenreIndex by viewModel.selectedGenreIndex.collectAsState() + val uiState by viewModel.uiState.collectAsState() val scrollState = rememberScrollState() - val isRefreshing by viewModel.isRefreshing.collectAsState() - val roomSectionsError by viewModel.roomSectionsError.collectAsState() - val showToast by viewModel.showToast.collectAsState() - val toastMessage by viewModel.toastMessage.collectAsState() Box( modifier = Modifier.fillMaxSize() ) { PullToRefreshBox( - isRefreshing = isRefreshing, + isRefreshing = uiState.isRefreshing, onRefresh = { viewModel.refreshGroupData() }, @@ -97,7 +91,7 @@ fun GroupScreen( Spacer(Modifier.height(20.dp)) GroupPager( - groupCards = myGroups, + groupCards = uiState.myGroups, onCardClick = { groupCard -> onNavigateToGroupRoom(groupCard.id) }, @@ -117,9 +111,9 @@ fun GroupScreen( // 마감 임박한 독서 모임방 GroupRoomDeadlineSection( - roomSections = roomSections, - selectedGenreIndex = selectedGenreIndex, - errorMessage = roomSectionsError, + roomSections = uiState.roomSections, + selectedGenreIndex = uiState.selectedGenreIndex, + errorMessage = uiState.roomSectionsError, onGenreSelect = { genreIndex -> viewModel.selectGenre(genreIndex) }, @@ -141,9 +135,9 @@ fun GroupScreen( ) // 토스트 팝업 - if (showToast) { + if (uiState.showToast) { ToastWithDate( - message = toastMessage, + message = uiState.toastMessage, modifier = Modifier .align(Alignment.TopCenter) .padding(horizontal = 20.dp, vertical = 16.dp) @@ -153,8 +147,8 @@ fun GroupScreen( } // 토스트 3초 후 자동 숨김 - showToast가 true가 된 시점부터 카운트 - LaunchedEffect(showToast) { - if (showToast) { + LaunchedEffect(uiState.showToast) { + if (uiState.showToast) { delay(3000L) viewModel.hideToast() } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 124c8f34..4537aef6 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -19,6 +19,7 @@ import com.texthip.thip.ui.group.screen.GroupScreen import com.texthip.thip.ui.group.search.screen.GroupSearchScreen import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel +import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState import com.texthip.thip.ui.navigator.extensions.navigateToAlarm import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom @@ -106,7 +107,7 @@ fun NavGraphBuilder.groupNavigation( GroupMyScreen( viewModel = groupMyViewModel, onCardClick = { room -> - val isRecruiting = room.type == "recruiting" + val isRecruiting = room.type == GroupMyUiState.RECRUITING if (isRecruiting) { navController.navigateToGroupRecruit(room.roomId) } else { @@ -122,10 +123,10 @@ fun NavGraphBuilder.groupNavigation( // Group Search 화면 composable { val groupViewModel: GroupViewModel = hiltViewModel() - val searchGroups by groupViewModel.searchGroups.collectAsState() + val uiState by groupViewModel.uiState.collectAsState() GroupSearchScreen( - roomList = searchGroups, + roomList = uiState.roomSections.flatMap { it.rooms }, //임시 onNavigateBack = { navigateBack() }, From 4f5545828d66c86bcb4714e9d6f3d820dabf8ccd Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 00:09:36 +0900 Subject: [PATCH 53/68] =?UTF-8?q?[refactor]:=20object=EB=A5=BC=20enum=20cl?= =?UTF-8?q?ass=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../done/{Screen => screen}/GroupDoneScreen.kt | 2 +- .../thip/ui/group/myroom/mock/GroupMyUiState.kt | 13 +------------ .../texthip/thip/ui/group/myroom/mock/RoomType.kt | 8 ++++++++ .../texthip/thip/ui/group/room/mock/RoomAction.kt | 6 ++++++ 4 files changed, 16 insertions(+), 13 deletions(-) rename app/src/main/java/com/texthip/thip/ui/group/done/{Screen => screen}/GroupDoneScreen.kt (98%) create mode 100644 app/src/main/java/com/texthip/thip/ui/group/myroom/mock/RoomType.kt create mode 100644 app/src/main/java/com/texthip/thip/ui/group/room/mock/RoomAction.kt diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt similarity index 98% rename from app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt rename to app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt index 414285e6..45799d49 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/Screen/GroupDoneScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.ui.group.done.Screen +package com.texthip.thip.ui.group.done.screen import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt index 70746400..74992c2e 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt @@ -4,22 +4,11 @@ import com.texthip.thip.ui.group.done.mock.MyRoomCardData data class GroupMyUiState( val myRooms: List = emptyList(), - val currentRoomType: String = PLAYING_AND_RECRUITING, + val currentRoomType: RoomType = RoomType.PLAYING_AND_RECRUITING, val isLoading: Boolean = false, val isLoadingMore: Boolean = false, val error: String? = null ) { val hasContent: Boolean get() = myRooms.isNotEmpty() val canLoadMore: Boolean get() = !isLoading && !isLoadingMore - - companion object { - const val PLAYING_AND_RECRUITING = "playingAndRecruiting" - const val RECRUITING = "recruiting" - const val PLAYING = "playing" - const val EXPIRED = "expired" - - // Room actions - const val ACTION_JOIN = "join" - const val ACTION_CANCEL = "cancel" - } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/RoomType.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/RoomType.kt new file mode 100644 index 00000000..fe71e4f1 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/RoomType.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.ui.group.myroom.mock + +enum class RoomType(val value: String) { + PLAYING_AND_RECRUITING("playingAndRecruiting"), + RECRUITING("recruiting"), + PLAYING("playing"), + EXPIRED("expired") +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/mock/RoomAction.kt b/app/src/main/java/com/texthip/thip/ui/group/room/mock/RoomAction.kt new file mode 100644 index 00000000..a7a8861b --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/room/mock/RoomAction.kt @@ -0,0 +1,6 @@ +package com.texthip.thip.ui.group.room.mock + +enum class RoomAction(val value: String) { + JOIN("join"), + CANCEL("cancel") +} \ No newline at end of file From 5b0e16b484d984c50aa40b1ee8506701e34ec3d3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 00:18:16 +0900 Subject: [PATCH 54/68] =?UTF-8?q?[refactor]:=20object=EB=A5=BC=20enum=20cl?= =?UTF-8?q?ass=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/group/done/mock/MyRoomCardData.kt | 10 +++++----- .../done/viewmodel/GroupDoneViewModel.kt | 4 ++-- .../ui/group/myroom/screen/GroupMyScreen.kt | 20 +++++++++---------- .../myroom/viewmodel/GroupMyViewModel.kt | 5 +++-- .../viewmodel/GroupRoomRecruitViewModel.kt | 6 +++--- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt index 005e31f7..9fa50ee4 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -1,7 +1,7 @@ package com.texthip.thip.ui.group.done.mock import com.texthip.thip.data.mapper.GroupDataMapper -import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState +import com.texthip.thip.ui.group.myroom.mock.RoomType data class MyRoomCardData( val roomId: Int, @@ -22,10 +22,10 @@ data class MyRoomsPaginationResult( // 타입 기반 모집 상태 확인 함수 fun MyRoomCardData.isRecruitingByType(): Boolean { return when (type) { - GroupMyUiState.RECRUITING -> true - GroupMyUiState.PLAYING_AND_RECRUITING -> false - GroupMyUiState.PLAYING -> false - GroupMyUiState.EXPIRED -> false + RoomType.RECRUITING.value -> true + RoomType.PLAYING_AND_RECRUITING.value -> false + RoomType.PLAYING.value -> false + RoomType.EXPIRED.value -> false else -> false } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index 7edd3db1..b6f8eea8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.done.mock.GroupDoneUiState -import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState +import com.texthip.thip.ui.group.myroom.mock.RoomType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -60,7 +60,7 @@ class GroupDoneViewModel @Inject constructor( isLoadingMore = true } - repository.getMyRoomsByType(GroupMyUiState.EXPIRED, nextCursor) + repository.getMyRoomsByType(RoomType.EXPIRED.value, nextCursor) .onSuccess { paginationResult -> val currentList = if (reset) emptyList() else uiState.value.expiredRooms updateState { diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index fc0882ec..715bd6c4 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -29,11 +29,11 @@ import com.texthip.thip.R import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.group.done.mock.MyRoomCardData -import com.texthip.thip.ui.group.done.mock.isRecruitingByType import com.texthip.thip.ui.group.done.mock.getEndDateInDays +import com.texthip.thip.ui.group.done.mock.isRecruitingByType import com.texthip.thip.ui.group.myroom.component.GroupMyRoomFilterRow +import com.texthip.thip.ui.group.myroom.mock.RoomType import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel -import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -65,8 +65,8 @@ fun GroupMyScreen( // Filter 상태를 val selectedStates = remember(uiState.currentRoomType) { when (uiState.currentRoomType) { - GroupMyUiState.PLAYING -> booleanArrayOf(true, false) - GroupMyUiState.RECRUITING -> booleanArrayOf(false, true) + RoomType.PLAYING -> booleanArrayOf(true, false) + RoomType.RECRUITING -> booleanArrayOf(false, true) else -> booleanArrayOf(false, false) // playingAndRecruiting } } @@ -101,23 +101,21 @@ fun GroupMyScreen( idx == 0 -> { if (selectedStates[0]) { // 이미 선택된 상태면 전체로 변경 - GroupMyUiState.PLAYING_AND_RECRUITING + RoomType.PLAYING_AND_RECRUITING } else { // 선택되지 않은 상태면 진행중만 - GroupMyUiState.PLAYING + RoomType.PLAYING } } // 모집중 버튼을 눌렀을 때 idx == 1 -> { if (selectedStates[1]) { - // 이미 선택된 상태면 전체로 변경 - GroupMyUiState.PLAYING_AND_RECRUITING + RoomType.PLAYING_AND_RECRUITING } else { - // 선택되지 않은 상태면 모집중만 - GroupMyUiState.RECRUITING + RoomType.RECRUITING } } - else -> GroupMyUiState.PLAYING_AND_RECRUITING + else -> RoomType.PLAYING_AND_RECRUITING } viewModel.changeRoomType(newRoomType) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt index 320af562..73c3d2df 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState +import com.texthip.thip.ui.group.myroom.mock.RoomType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -47,7 +48,7 @@ class GroupMyViewModel @Inject constructor( updateState { it.copy(isLoadingMore = true) } } - repository.getMyRoomsByType(uiState.value.currentRoomType, nextCursor) + repository.getMyRoomsByType(uiState.value.currentRoomType.value, nextCursor) .onSuccess { paginationResult -> val currentList = if (reset) emptyList() else uiState.value.myRooms updateState { @@ -77,7 +78,7 @@ class GroupMyViewModel @Inject constructor( loadMyRooms(reset = true) } - fun changeRoomType(roomType: String) { + fun changeRoomType(roomType: RoomType) { if (roomType != uiState.value.currentRoomType) { updateState { it.copy(currentRoomType = roomType) } loadMyRooms(reset = true) diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index 08dc37f7..76d0bcca 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState import com.texthip.thip.ui.group.room.mock.GroupRoomRecruitUiState +import com.texthip.thip.ui.group.room.mock.RoomAction import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -51,7 +51,7 @@ class GroupRoomRecruitViewModel @Inject constructor( viewModelScope.launch { val roomId = uiState.value.roomDetail?.id ?: return@launch - repository.joinOrCancelRoom(roomId, GroupMyUiState.ACTION_JOIN) + repository.joinOrCancelRoom(roomId, RoomAction.JOIN.value) .onSuccess { updateState { it.copy(currentButtonType = GroupBottomButtonType.CANCEL) } showToastMessage("참여가 완료되었습니다") @@ -74,7 +74,7 @@ class GroupRoomRecruitViewModel @Inject constructor( viewModelScope.launch { val roomId = uiState.value.roomDetail?.id ?: return@launch - repository.joinOrCancelRoom(roomId, GroupMyUiState.ACTION_CANCEL) + repository.joinOrCancelRoom(roomId, RoomAction.CANCEL.value) .onSuccess { updateState { it.copy( From 94ca51c4aff0cf21a3f41320e4afe83a0738ec36 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 00:18:38 +0900 Subject: [PATCH 55/68] =?UTF-8?q?[refactor]:=20launchedEffect=EA=B0=80=20?= =?UTF-8?q?=EA=B3=84=EC=86=8D=20=EC=8B=A4=ED=96=89=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/navigator/navigations/GroupNavigation.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 4537aef6..00a43e0f 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -9,7 +9,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.texthip.thip.ui.group.done.Screen.GroupDoneScreen +import com.texthip.thip.ui.group.done.screen.GroupDoneScreen import com.texthip.thip.ui.group.makeroom.screen.GroupMakeRoomScreen import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel import com.texthip.thip.ui.group.myroom.screen.GroupMyScreen @@ -19,7 +19,7 @@ import com.texthip.thip.ui.group.screen.GroupScreen import com.texthip.thip.ui.group.search.screen.GroupSearchScreen import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel -import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState +import com.texthip.thip.ui.group.myroom.mock.RoomType import com.texthip.thip.ui.navigator.extensions.navigateToAlarm import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom @@ -42,7 +42,7 @@ fun NavGraphBuilder.groupNavigation( val groupViewModel: GroupViewModel = hiltViewModel() // 네비게이션 파라미터로 전달된 토스트 메시지가 있는지 확인 - LaunchedEffect(Unit) { + LaunchedEffect(backStackEntry) { val toastMessage = backStackEntry.savedStateHandle.get("toast_message") toastMessage?.let { message -> @@ -107,7 +107,7 @@ fun NavGraphBuilder.groupNavigation( GroupMyScreen( viewModel = groupMyViewModel, onCardClick = { room -> - val isRecruiting = room.type == GroupMyUiState.RECRUITING + val isRecruiting = room.type == RoomType.RECRUITING.value if (isRecruiting) { navController.navigateToGroupRecruit(room.roomId) } else { From dc94a55c679a0a9b0259d413b02e5bf5dae73ef4 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 00:34:15 +0900 Subject: [PATCH 56/68] =?UTF-8?q?[refactor]:=20StringResource=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/data/manager/GenreManager.kt | 2 +- .../thip/data/manager/UserDataManager.kt | 9 +-------- .../viewmodel/GroupMakeRoomViewModel.kt | 14 +++++++++----- .../room/viewmodel/GroupRoomRecruitViewModel.kt | 16 ++++++++++------ .../thip/ui/group/viewmodel/GroupViewModel.kt | 10 +++++++--- .../ui/navigator/navigations/GroupNavigation.kt | 2 -- app/src/main/res/values/strings.xml | 17 +++++++++++++++++ 7 files changed, 45 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt index 9426906f..bbda20c4 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt @@ -25,7 +25,7 @@ class GenreManager @Inject constructor( fun mapGenreToApiCategory(genre: String): String { return when (genre) { - "과학·IT" -> "과학/IT" + context.getString(R.string.science_it) -> context.getString(R.string.api_genre_science_it) else -> genre } } diff --git a/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt b/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt index e00ab505..11cfd151 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt @@ -15,12 +15,5 @@ class UserDataManager @Inject constructor() { fun cacheUserName(name: String) { cachedUserName = name } - - fun clearUserData() { - cachedUserName = null - } - - fun hasUserName(): Boolean { - return cachedUserName != null - } + } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index fe8f5329..6141206c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -1,7 +1,9 @@ package com.texthip.thip.ui.group.makeroom.viewmodel +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.R import com.texthip.thip.data.model.book.response.BookSavedResponse import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.data.model.group.request.CreateRoomRequest @@ -9,6 +11,7 @@ import com.texthip.thip.data.repository.BookRepository import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -20,7 +23,8 @@ import javax.inject.Inject @HiltViewModel class GroupMakeRoomViewModel @Inject constructor( private val groupRepository: GroupRepository, - private val bookRepository: BookRepository + private val bookRepository: BookRepository, + @param:ApplicationContext private val context: Context ) : ViewModel() { private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) @@ -130,13 +134,13 @@ class GroupMakeRoomViewModel @Inject constructor( val currentState = _uiState.value if (!currentState.isFormValid) { - onError("입력 정보를 확인해주세요") + onError(context.getString(R.string.error_form_validation)) return } val selectedBook = currentState.selectedBook if (selectedBook?.isbn == null) { - onError("책 정보가 올바르지 않습니다") + onError(context.getString(R.string.error_book_info_invalid)) return } @@ -160,10 +164,10 @@ class GroupMakeRoomViewModel @Inject constructor( result.onSuccess { roomId -> onSuccess(roomId) }.onFailure { exception -> - onError("모임방 생성에 실패했습니다: ${exception.message}") + onError(context.getString(R.string.error_room_creation_failed, exception.message ?: "")) } } catch (e: Exception) { - onError("네트워크 오류가 발생했습니다: ${e.message}") + onError(context.getString(R.string.error_network_error, e.message ?: "")) } finally { updateState { it.copy(isLoading = false) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index 76d0bcca..64aa3af5 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -1,12 +1,15 @@ package com.texthip.thip.ui.group.room.viewmodel +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.R import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.room.mock.GroupRoomRecruitUiState import com.texthip.thip.ui.group.room.mock.RoomAction import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,7 +18,8 @@ import javax.inject.Inject @HiltViewModel class GroupRoomRecruitViewModel @Inject constructor( - private val repository: GroupRepository + private val repository: GroupRepository, + @param:ApplicationContext private val context: Context ) : ViewModel() { private val _uiState = MutableStateFlow(GroupRoomRecruitUiState()) @@ -54,10 +58,10 @@ class GroupRoomRecruitViewModel @Inject constructor( repository.joinOrCancelRoom(roomId, RoomAction.JOIN.value) .onSuccess { updateState { it.copy(currentButtonType = GroupBottomButtonType.CANCEL) } - showToastMessage("참여가 완료되었습니다") + showToastMessage(context.getString(R.string.success_participation_complete)) } .onFailure { error -> - showToastMessage("참여 처리 중 오류가 발생했습니다: ${error.message}") + showToastMessage(context.getString(R.string.error_participation_failed, error.message ?: "")) } } } @@ -79,14 +83,14 @@ class GroupRoomRecruitViewModel @Inject constructor( updateState { it.copy( currentButtonType = GroupBottomButtonType.JOIN, - toastMessage = "모임방 참여가 취소되었어요! 다른 방을 찾아보세요.", + toastMessage = context.getString(R.string.success_participation_cancelled), showToast = true, shouldNavigateToGroupScreen = true ) } } .onFailure { error -> - showToastMessage("참여 취소 중 오류가 발생했습니다: ${error.message}") + showToastMessage(context.getString(R.string.error_participation_cancel_failed)) } } } @@ -103,7 +107,7 @@ class GroupRoomRecruitViewModel @Inject constructor( pendingAction = { viewModelScope.launch { // TODO: 실제 모집 마감 API 호출 - showToastMessage("모집이 마감되었습니다") + showToastMessage(context.getString(R.string.success_recruitment_closed)) } } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index b41df540..c569f4f0 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -1,10 +1,13 @@ package com.texthip.thip.ui.group.viewmodel +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.R import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.mock.GroupUiState import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow @@ -15,7 +18,8 @@ import javax.inject.Inject @HiltViewModel class GroupViewModel @Inject constructor( - private val repository: GroupRepository + private val repository: GroupRepository, + @param:ApplicationContext private val context: Context ) : ViewModel() { private val _uiState = MutableStateFlow(GroupUiState()) @@ -121,10 +125,10 @@ class GroupViewModel @Inject constructor( if (selectedIndex >= 0 && selectedIndex < genres.size) { genres[selectedIndex] } else { - genres.firstOrNull() ?: "문학" + genres.firstOrNull() ?: context.getString(R.string.literature) } } else { - "문학" + context.getString(R.string.literature) } repository.getRoomSections(selectedGenre) diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 00a43e0f..f85de136 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -165,8 +165,6 @@ fun NavGraphBuilder.groupNavigation( // Group Room 화면 composable { backStackEntry -> val route = backStackEntry.toRoute() - val roomId = route.roomId - val groupViewModel: GroupViewModel = hiltViewModel() GroupRoomScreen( onBackClick = { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13623010..f138ef9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -351,4 +351,21 @@ 데이터를 불러올 수 없습니다 네트워크 연결을 확인해주세요 + + + 입력 정보를 확인해주세요 + 책 정보가 올바르지 않습니다 + 모임방 생성에 실패했습니다: %1$s + 네트워크 오류가 발생했습니다: %1$s + + + 참여가 완료되었습니다 + 참여 처리 중 오류가 발생했습니다: %1$s + 모임방 참여가 취소되었어요! 다른 방을 찾아보세요. + 참여 취소 중 오류가 발생했습니다: %1$s + 모집이 마감되었습니다 + + + 과학/IT + \ No newline at end of file From 2a1e8dd3f22f2354bb7b412582f4a086739ef44f Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 00:51:23 +0900 Subject: [PATCH 57/68] =?UTF-8?q?[refactor]:=20=EB=AC=B4=ED=95=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...okSavedResponse.kt => BookListResponse.kt} | 11 ++++---- .../ui/group/done/mock/GroupDoneUiState.kt | 3 ++ .../ui/group/done/screen/GroupDoneScreen.kt | 2 +- .../done/viewmodel/GroupDoneViewModel.kt | 28 +++++++++++++------ .../ui/group/myroom/mock/GroupMyUiState.kt | 3 +- .../ui/group/myroom/screen/GroupMyScreen.kt | 2 +- .../myroom/viewmodel/GroupMyViewModel.kt | 5 ++-- 7 files changed, 36 insertions(+), 18 deletions(-) rename app/src/main/java/com/texthip/thip/data/model/book/response/{BookSavedResponse.kt => BookListResponse.kt} (99%) diff --git a/app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt b/app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt similarity index 99% rename from app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt rename to app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt index 9526091b..54a61093 100644 --- a/app/src/main/java/com/texthip/thip/data/model/book/response/BookSavedResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt @@ -3,6 +3,12 @@ package com.texthip.thip.data.model.book.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable + +@Serializable +data class BookListResponse( + @SerialName("bookList") val bookList: List +) + @Serializable data class BookSavedResponse( @SerialName("isbn") val isbn: String, @@ -10,9 +16,4 @@ data class BookSavedResponse( @SerialName("authorName") val authorName: String, @SerialName("publisher") val publisher: String, @SerialName("imageUrl") val imageUrl: String? -) - -@Serializable -data class BookListResponse( - @SerialName("bookList") val bookList: List ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt index 6540f337..e92c1eb3 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt @@ -3,8 +3,11 @@ package com.texthip.thip.ui.group.done.mock data class GroupDoneUiState( val expiredRooms: List = emptyList(), val isLoading: Boolean = false, + val isLoadingMore: Boolean = false, + val hasMore: Boolean = true, val userName: String = "", val error: String? = null ) { val hasContent: Boolean get() = expiredRooms.isNotEmpty() + val canLoadMore: Boolean get() = !isLoading && !isLoadingMore && hasMore } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt index 45799d49..9cbd24c0 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt @@ -51,7 +51,7 @@ fun GroupDoneScreen( } LaunchedEffect(shouldLoadMore) { - if (shouldLoadMore && uiState.expiredRooms.isNotEmpty()) { + if (shouldLoadMore && uiState.canLoadMore) { viewModel.loadMoreExpiredRooms() } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index b6f8eea8..ef99f9be 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -23,6 +23,7 @@ class GroupDoneViewModel @Inject constructor( private var nextCursor: String? = null private var isLastPage = false private var isLoadingMore = false + private var isInitialLoading = false private fun updateState(update: (GroupDoneUiState) -> GroupDoneUiState) { _uiState.value = update(_uiState.value) @@ -47,17 +48,21 @@ class GroupDoneViewModel @Inject constructor( } fun loadExpiredRooms(reset: Boolean = false) { - if (isLoadingMore && !reset) return - if (isLastPage && !reset) return + // 중복 호출 방지 + if (reset) { + if (isInitialLoading) return + isInitialLoading = true + } else { + if (isLoadingMore || isLastPage) return + isLoadingMore = true + } viewModelScope.launch { try { if (reset) { - updateState { it.copy(isLoading = true, expiredRooms = emptyList()) } + updateState { it.copy(isLoading = true, expiredRooms = emptyList(), hasMore = true) } nextCursor = null isLastPage = false - } else { - isLoadingMore = true } repository.getMyRoomsByType(RoomType.EXPIRED.value, nextCursor) @@ -66,7 +71,9 @@ class GroupDoneViewModel @Inject constructor( updateState { it.copy( expiredRooms = currentList + paginationResult.data, - error = null + error = null, + isLoadingMore = false, + hasMore = !paginationResult.isLast ) } nextCursor = paginationResult.nextCursor @@ -76,8 +83,13 @@ class GroupDoneViewModel @Inject constructor( updateState { it.copy(error = exception.message) } } } finally { - updateState { it.copy(isLoading = false) } - isLoadingMore = false + if (reset) { + updateState { it.copy(isLoading = false) } + isInitialLoading = false + } else { + updateState { it.copy(isLoadingMore = false) } + isLoadingMore = false + } } } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt index 74992c2e..2b20541b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt @@ -7,8 +7,9 @@ data class GroupMyUiState( val currentRoomType: RoomType = RoomType.PLAYING_AND_RECRUITING, val isLoading: Boolean = false, val isLoadingMore: Boolean = false, + val hasMore: Boolean = true, val error: String? = null ) { val hasContent: Boolean get() = myRooms.isNotEmpty() - val canLoadMore: Boolean get() = !isLoading && !isLoadingMore + val canLoadMore: Boolean get() = !isLoading && !isLoadingMore && hasMore } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index 715bd6c4..31f4bd64 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -57,7 +57,7 @@ fun GroupMyScreen( } LaunchedEffect(shouldLoadMore) { - if (shouldLoadMore && uiState.myRooms.isNotEmpty()) { + if (shouldLoadMore && uiState.canLoadMore) { viewModel.loadMoreMyRooms() } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt index 73c3d2df..559db7de 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -41,7 +41,7 @@ class GroupMyViewModel @Inject constructor( isLoadingData = true if (reset) { - updateState { it.copy(isLoading = true, myRooms = emptyList()) } + updateState { it.copy(isLoading = true, myRooms = emptyList(), hasMore = true) } nextCursor = null isLastPage = false } else { @@ -54,7 +54,8 @@ class GroupMyViewModel @Inject constructor( updateState { it.copy( myRooms = currentList + paginationResult.data, - error = null + error = null, + hasMore = !paginationResult.isLast ) } nextCursor = paginationResult.nextCursor From b3e28e87db16610022c18c892d6eeaad54df2d0d Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 01:00:23 +0900 Subject: [PATCH 58/68] =?UTF-8?q?[refactor]:=20runCatching=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/repository/BookRepository.kt | 15 +- .../thip/data/repository/GroupRepository.kt | 201 ++++++++---------- .../thip/ui/common/buttons/GenreChipRow.kt | 7 +- 3 files changed, 95 insertions(+), 128 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt index 7333f420..3616bd94 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt @@ -12,15 +12,10 @@ class BookRepository @Inject constructor( ) { /** 저장된 책 또는 모임 책 목록 조회 */ - suspend fun getBooks(type: String): Result> { - return try { - bookService.getBooks(type) - .handleBaseResponse() - .mapCatching { bookListResponse -> - bookListResponse?.bookList ?: emptyList() - } - } catch (e: Exception) { - Result.failure(e) - } + suspend fun getBooks(type: String) = runCatching { + bookService.getBooks(type) + .handleBaseResponse() + .getOrThrow() + ?.bookList ?: emptyList() } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt index 0e784b94..c4cfd7df 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt @@ -38,138 +38,107 @@ class GroupRepository @Inject constructor( } /** 내가 참여 중인 모임방 목록 조회 */ - suspend fun getMyJoinedRooms(page: Int): Result> { - return try { - groupService.getJoinedRooms(page) - .handleBaseResponse() - .mapCatching { data -> - data?.let { joinedRoomsDto -> - userDataManager.cacheUserName(joinedRoomsDto.nickname) - - val groups = joinedRoomsDto.roomList.map { dto -> - groupDataMapper.toGroupCardData(dto, joinedRoomsDto.nickname) - } - - PaginationResult( - data = groups, - hasMore = !joinedRoomsDto.last, - currentPage = joinedRoomsDto.page, - nickname = joinedRoomsDto.nickname - ) - } ?: PaginationResult( - data = emptyList(), - hasMore = false, - currentPage = page, - nickname = "" - ) + suspend fun getMyJoinedRooms(page: Int) = runCatching { + groupService.getJoinedRooms(page) + .handleBaseResponse() + .getOrThrow() + ?.let { joinedRoomsDto -> + userDataManager.cacheUserName(joinedRoomsDto.nickname) + + val groups = joinedRoomsDto.roomList.map { dto -> + groupDataMapper.toGroupCardData(dto, joinedRoomsDto.nickname) } - } catch (e: Exception) { - Result.failure(e) - } + + PaginationResult( + data = groups, + hasMore = !joinedRoomsDto.last, + currentPage = joinedRoomsDto.page, + nickname = joinedRoomsDto.nickname + ) + } ?: PaginationResult( + data = emptyList(), + hasMore = false, + currentPage = page, + nickname = "" + ) } /** 카테고리별 모임방 섹션 조회 (마감임박/인기) */ - suspend fun getRoomSections(category: String = ""): Result> { - return try { - val finalCategory = category.ifEmpty { genreManager.getDefaultGenre() } - val apiCategory = genreManager.mapGenreToApiCategory(finalCategory) - - groupService.getRooms(apiCategory) - .handleBaseResponse() - .mapCatching { data -> - data?.let { roomsData -> - val sections = listOf( - GroupRoomSectionData( - title = context.getString(R.string.room_section_deadline), - rooms = roomsData.deadlineRoomList.map { dto -> - val daysLeft = groupDataMapper.extractDaysFromDeadline(dto.deadlineDate) - groupDataMapper.toGroupCardItemRoomData(dto, daysLeft) - }, - genres = genreManager.getGenres() - ), - GroupRoomSectionData( - title = context.getString(R.string.room_section_popular), - rooms = roomsData.popularRoomList.map { dto -> - val daysLeft = groupDataMapper.extractDaysFromDeadline(dto.deadlineDate) - groupDataMapper.toGroupCardItemRoomData(dto, daysLeft) - }, - genres = genreManager.getGenres() - ) - ) - sections - }.orEmpty() - } - } catch (e: Exception) { - Result.failure(e) - } + suspend fun getRoomSections(category: String = "") = runCatching { + val finalCategory = category.ifEmpty { genreManager.getDefaultGenre() } + val apiCategory = genreManager.mapGenreToApiCategory(finalCategory) + + groupService.getRooms(apiCategory) + .handleBaseResponse() + .getOrThrow() + ?.let { roomsData -> + listOf( + GroupRoomSectionData( + title = context.getString(R.string.room_section_deadline), + rooms = roomsData.deadlineRoomList.map { dto -> + val daysLeft = groupDataMapper.extractDaysFromDeadline(dto.deadlineDate) + groupDataMapper.toGroupCardItemRoomData(dto, daysLeft) + }, + genres = genreManager.getGenres() + ), + GroupRoomSectionData( + title = context.getString(R.string.room_section_popular), + rooms = roomsData.popularRoomList.map { dto -> + val daysLeft = groupDataMapper.extractDaysFromDeadline(dto.deadlineDate) + groupDataMapper.toGroupCardItemRoomData(dto, daysLeft) + }, + genres = genreManager.getGenres() + ) + ) + }.orEmpty() } /** 타입별 내 모임방 목록 조회 */ - suspend fun getMyRoomsByType(type: String?, cursor: String? = null): Result { - return try { - groupService.getMyRooms(type, cursor) - .handleBaseResponse() - .mapCatching { myRoomsDto -> - myRoomsDto?.let { data -> - val myRoomCards = data.roomList.map { room -> - groupDataMapper.toMyRoomCardData(room) - } - - MyRoomsPaginationResult( - data = myRoomCards, - nextCursor = data.nextCursor, - isLast = data.isLast - ) - } ?: MyRoomsPaginationResult( - data = emptyList(), - nextCursor = null, - isLast = true - ) + suspend fun getMyRoomsByType(type: String?, cursor: String? = null) = runCatching { + groupService.getMyRooms(type, cursor) + .handleBaseResponse() + .getOrThrow() + ?.let { data -> + val myRoomCards = data.roomList.map { room -> + groupDataMapper.toMyRoomCardData(room) } - } catch (e: Exception) { - Result.failure(e) - } + + MyRoomsPaginationResult( + data = myRoomCards, + nextCursor = data.nextCursor, + isLast = data.isLast + ) + } ?: MyRoomsPaginationResult( + data = emptyList(), + nextCursor = null, + isLast = true + ) } /** 모집중인 모임방 상세 정보 조회 */ - suspend fun getRoomRecruiting(roomId: Int): Result { - return try { - groupService.getRoomRecruiting(roomId) - .handleBaseResponse() - .mapCatching { recruitingDto -> - recruitingDto?.let { data -> - groupDataMapper.toGroupRoomData(data) - } ?: throw Exception("No recruiting data found for room $roomId") - } - } catch (e: Exception) { - Result.failure(e) - } + suspend fun getRoomRecruiting(roomId: Int) = runCatching { + groupService.getRoomRecruiting(roomId) + .handleBaseResponse() + .getOrThrow() + ?.let { data -> + groupDataMapper.toGroupRoomData(data) + } ?: throw Exception("No recruiting data found for room $roomId") } /** 새 모임방 생성 */ - suspend fun createRoom(request: CreateRoomRequest): Result { - return try { - groupService.createRoom(request) - .handleBaseResponse() - .mapCatching { createRoomResponse -> - createRoomResponse?.roomId ?: throw Exception("Failed to create room: roomId is null") - } - } catch (e: Exception) { - Result.failure(e) - } + suspend fun createRoom(request: CreateRoomRequest) = runCatching { + groupService.createRoom(request) + .handleBaseResponse() + .getOrThrow() + ?.roomId ?: throw Exception("Failed to create room: roomId is null") } /** 모임방 참여 또는 취소 */ - suspend fun joinOrCancelRoom(roomId: Int, type: String): Result { - return try { - val request = RoomJoinRequest(type = type) - groupService.joinOrCancelRoom(roomId, request) - .handleBaseResponse() - .mapCatching { response -> - response?.type ?: throw Exception("Failed to join/cancel room: no response") - } - } catch (e: Exception) { - Result.failure(e) - } + suspend fun joinOrCancelRoom(roomId: Int, type: String) = runCatching { + val request = RoomJoinRequest(type = type) + groupService.joinOrCancelRoom(roomId, request) + .handleBaseResponse() + .getOrThrow() + ?.type ?: throw Exception("Failed to join/cancel room: no response") } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt index ed259267..364cac9f 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt @@ -1,7 +1,10 @@ package com.texthip.thip.ui.common.buttons -import android.graphics.Color -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier From f1795522c420fff674525aad01288a60e78d390c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 01:02:31 +0900 Subject: [PATCH 59/68] =?UTF-8?q?[refactor]:=20UiState=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/done/{mock => viewmodel}/GroupDoneUiState.kt | 4 +++- .../thip/ui/group/done/viewmodel/GroupDoneViewModel.kt | 2 +- .../makeroom/{mock => viewmodel}/GroupMakeRoomUiState.kt | 4 +++- .../ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt | 2 +- .../ui/group/myroom/{mock => viewmodel}/GroupMyUiState.kt | 3 ++- .../thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt | 2 +- .../group/room/{mock => viewmodel}/GroupRoomRecruitUiState.kt | 2 +- .../thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt | 2 +- .../texthip/thip/ui/group/{mock => viewmodel}/GroupUiState.kt | 2 +- .../com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt | 2 +- 10 files changed, 15 insertions(+), 10 deletions(-) rename app/src/main/java/com/texthip/thip/ui/group/done/{mock => viewmodel}/GroupDoneUiState.kt (78%) rename app/src/main/java/com/texthip/thip/ui/group/makeroom/{mock => viewmodel}/GroupMakeRoomUiState.kt (91%) rename app/src/main/java/com/texthip/thip/ui/group/myroom/{mock => viewmodel}/GroupMyUiState.kt (82%) rename app/src/main/java/com/texthip/thip/ui/group/room/{mock => viewmodel}/GroupRoomRecruitUiState.kt (92%) rename app/src/main/java/com/texthip/thip/ui/group/{mock => viewmodel}/GroupUiState.kt (94%) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt similarity index 78% rename from app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt rename to app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt index e92c1eb3..47e32828 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/GroupDoneUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt @@ -1,4 +1,6 @@ -package com.texthip.thip.ui.group.done.mock +package com.texthip.thip.ui.group.done.viewmodel + +import com.texthip.thip.ui.group.done.mock.MyRoomCardData data class GroupDoneUiState( val expiredRooms: List = emptyList(), diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index ef99f9be..48d308c5 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -3,7 +3,7 @@ package com.texthip.thip.ui.group.done.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.done.mock.GroupDoneUiState +import com.texthip.thip.ui.group.done.viewmodel.GroupDoneUiState import com.texthip.thip.ui.group.myroom.mock.RoomType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt similarity index 91% rename from app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt rename to app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt index 217c9187..7cdf3601 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt @@ -1,5 +1,7 @@ -package com.texthip.thip.ui.group.makeroom.mock +package com.texthip.thip.ui.group.makeroom.viewmodel +import com.texthip.thip.ui.group.makeroom.mock.BookData +import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomRequest import java.time.LocalDate import java.time.temporal.ChronoUnit diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index 6141206c..511274eb 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -9,7 +9,7 @@ import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.data.repository.BookRepository import com.texthip.thip.ui.group.makeroom.mock.BookData -import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState +import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomUiState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt similarity index 82% rename from app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt rename to app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt index 2b20541b..937df8f8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupMyUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt @@ -1,6 +1,7 @@ -package com.texthip.thip.ui.group.myroom.mock +package com.texthip.thip.ui.group.myroom.viewmodel import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import com.texthip.thip.ui.group.myroom.mock.RoomType data class GroupMyUiState( val myRooms: List = emptyList(), diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt index 559db7de..f1b979e6 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -3,7 +3,7 @@ package com.texthip.thip.ui.group.myroom.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.myroom.mock.GroupMyUiState +import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyUiState import com.texthip.thip.ui.group.myroom.mock.RoomType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/mock/GroupRoomRecruitUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt similarity index 92% rename from app/src/main/java/com/texthip/thip/ui/group/room/mock/GroupRoomRecruitUiState.kt rename to app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt index 1cad5265..1f7fa194 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/mock/GroupRoomRecruitUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.ui.group.room.mock +package com.texthip.thip.ui.group.room.viewmodel import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType import com.texthip.thip.ui.group.myroom.mock.GroupRoomData diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index 64aa3af5..937e639a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.viewModelScope import com.texthip.thip.R import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.room.mock.GroupRoomRecruitUiState +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitUiState import com.texthip.thip.ui.group.room.mock.RoomAction import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext diff --git a/app/src/main/java/com/texthip/thip/ui/group/mock/GroupUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt similarity index 94% rename from app/src/main/java/com/texthip/thip/ui/group/mock/GroupUiState.kt rename to app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt index d4482c8b..a5f7d6cf 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/mock/GroupUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.ui.group.mock +package com.texthip.thip.ui.group.viewmodel import com.texthip.thip.ui.group.myroom.mock.GroupCardData import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index c569f4f0..ae395813 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.R import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.mock.GroupUiState +import com.texthip.thip.ui.group.viewmodel.GroupUiState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.async From 0fc0b92abdd0a03aa55cc64cc1a8854d4ab768a7 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 01:03:47 +0900 Subject: [PATCH 60/68] =?UTF-8?q?[refactor]:=20drawable=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/common/cards/CardBookList.kt | 2 +- .../texthip/thip/ui/common/cards/CardBookSearch.kt | 2 +- .../texthip/thip/ui/common/cards/CardInputBook.kt | 2 +- .../texthip/thip/ui/common/cards/CardItemRoom.kt | 2 +- .../texthip/thip/ui/common/cards/CardRoomBook.kt | 4 +--- .../texthip/thip/ui/feed/component/MyFeedCard.kt | 2 +- .../thip/ui/feed/screen/FeedCommentScreen.kt | 4 ++-- .../com/texthip/thip/ui/feed/screen/FeedScreen.kt | 3 +-- .../ui/group/makeroom/component/GroupSelectBook.kt | 2 +- .../thip/ui/group/myroom/component/GroupMainCard.kt | 2 +- .../thip/ui/mypage/component/SavedFeedCard.kt | 2 +- .../thip/ui/mypage/viewmodel/SavedFeedViewModel.kt | 6 +++--- .../texthip/thip/ui/search/mock/DetailBookData.kt | 2 +- .../thip/ui/search/screen/SearchBookDetailScreen.kt | 2 +- ...okcover_sample.png => img_book_cover_sample.png} | Bin 15 files changed, 17 insertions(+), 20 deletions(-) rename app/src/main/res/drawable/{bookcover_sample.png => img_book_cover_sample.png} (100%) 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 87c8be00..717dd18f 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 @@ -44,7 +44,7 @@ fun CardBookList( ) { // 책 이미지 AsyncImage( - model = imageUrl ?: R.drawable.bookcover_sample, + model = imageUrl ?: R.drawable.img_book_cover_sample, contentDescription = "책 이미지", modifier = Modifier.size(width = 80.dp, height = 108.dp), contentScale = ContentScale.Crop diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt index d431ca07..15cb00a6 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt @@ -47,7 +47,7 @@ fun CardBookSearch( // 이미지 AsyncImage( - model = imageUrl ?: R.drawable.bookcover_sample, + model = imageUrl ?: R.drawable.img_book_cover_sample, contentDescription = "책 이미지", modifier = Modifier.size(width = 45.dp, height = 60.dp), contentScale = ContentScale.Crop diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt index 5a59a36f..bfffacff 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt @@ -34,7 +34,7 @@ fun CardInputBook( modifier: Modifier = Modifier, title: String, author: String, - imageRes: Int? = R.drawable.bookcover_sample, // 기본 이미지 리소스 + imageRes: Int? = R.drawable.img_book_cover_sample, // 기본 이미지 리소스 onChangeClick: () -> Unit = {} ) { Row( diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt index ce4cb3d7..c4cda1cc 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt @@ -73,7 +73,7 @@ fun CardItemRoom( ) { // 이미지 AsyncImage( - model = imageUrl ?: R.drawable.bookcover_sample, + model = imageUrl ?: R.drawable.img_book_cover_sample, contentDescription = "책 이미지", modifier = Modifier.size(width = 80.dp, height = 107.dp), contentScale = ContentScale.Crop diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt index 79e10c68..a5b452b7 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt @@ -2,11 +2,9 @@ package com.texthip.thip.ui.common.cards import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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 @@ -88,7 +86,7 @@ fun CardRoomBook( ) { // 책 이미지 AsyncImage( - model = imageUrl ?: R.drawable.bookcover_sample, + model = imageUrl ?: R.drawable.img_book_cover_sample, contentDescription = "책 이미지", modifier = Modifier.size(width = 80.dp, height = 107.dp), contentScale = ContentScale.Crop diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt index e7646f43..e62a7094 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt @@ -142,7 +142,7 @@ private fun MyFeedCardPrev() { isLiked = false, isSaved = true, isLocked = false, - imageUrl = R.drawable.bookcover_sample + imageUrl = R.drawable.img_book_cover_sample ) Column { diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt index 9a587288..4cb2a3a2 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt @@ -377,10 +377,10 @@ private fun FeedCommentScreenPrev() { isLiked = true, isSaved = false, isLocked = true, - imageUrl = R.drawable.bookcover_sample, + imageUrl = R.drawable.img_book_cover_sample, tags = listOf("에세이", "문학", "힐링") ), - bookImage = painterResource(R.drawable.bookcover_sample), + bookImage = painterResource(R.drawable.img_book_cover_sample), profileImage = painterResource(R.drawable.character_literature), onLikeClick = {}, onCommentInputChange = {}, 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 131da9d6..7b918719 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 @@ -18,7 +18,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -269,7 +268,7 @@ private fun FeedScreenPreview() { isLiked = false, isSaved = false, isLocked = it % 2 == 0, - imageUrl = R.drawable.bookcover_sample + imageUrl = R.drawable.img_book_cover_sample ) } val mockFollowerImages = listOf( diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt index a16d9a48..98115678 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt @@ -85,7 +85,7 @@ fun GroupSelectBook( verticalAlignment = Alignment.Bottom ) { AsyncImage( - model = selectedBook.imageUrl ?: R.drawable.bookcover_sample, + model = selectedBook.imageUrl ?: R.drawable.img_book_cover_sample, contentDescription = selectedBook.title, modifier = Modifier .height(80.dp) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt index ef35f4f9..6b857291 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt @@ -75,7 +75,7 @@ fun GroupMainCard( ) { // 책 이미지 AsyncImage( - model = data.imageUrl ?: R.drawable.bookcover_sample, + model = data.imageUrl ?: R.drawable.img_book_cover_sample, contentDescription = "책 이미지", modifier = Modifier .size(width = 80.dp, height = 107.dp), 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 5d82f201..c4854dae 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 @@ -148,7 +148,7 @@ private fun SavedFeedCardPrev() { commentCount = 5, isLiked = false, isSaved = true, - imageUrl = R.drawable.bookcover_sample + imageUrl = R.drawable.img_book_cover_sample ) Column { diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedFeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedFeedViewModel.kt index 1eefdc48..204b5671 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedFeedViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedFeedViewModel.kt @@ -36,7 +36,7 @@ class SavedFeedViewModel: ViewModel() { commentCount = 4, isLiked = false, isSaved = true, - imageUrl = R.drawable.bookcover_sample + imageUrl = R.drawable.img_book_cover_sample ), FeedItem( id = 3, @@ -51,7 +51,7 @@ class SavedFeedViewModel: ViewModel() { commentCount = 4, isLiked = false, isSaved = true, - imageUrl = R.drawable.bookcover_sample + imageUrl = R.drawable.img_book_cover_sample ), FeedItem( id = 4, @@ -66,7 +66,7 @@ class SavedFeedViewModel: ViewModel() { commentCount = 4, isLiked = false, isSaved = true, - imageUrl = R.drawable.bookcover_sample + imageUrl = R.drawable.img_book_cover_sample ), FeedItem( id = 5, diff --git a/app/src/main/java/com/texthip/thip/ui/search/mock/DetailBookData.kt b/app/src/main/java/com/texthip/thip/ui/search/mock/DetailBookData.kt index b30df944..dee82adb 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/mock/DetailBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/mock/DetailBookData.kt @@ -7,7 +7,7 @@ data class DetailBookData( val author: String, val publisher: String, val description: String, - val coverImageRes: Int? = R.drawable.bookcover_sample, + val coverImageRes: Int? = R.drawable.img_book_cover_sample, val participantsCount: Int = 0, val recruitingRoomCount: Int = 0 ) diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt index 29bf101c..59acf186 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt @@ -333,7 +333,7 @@ fun PreviewBookDetailScreen() { description = "인터내셔널 북커상, 산클레멘테 문학상 수상작. 전세계가 주목한 인간의 역작을 다시 만나다.2016년 인터내셔널 북커상을 수상하며 한국문학의 입지를 한단계 확장시킨 한강의 명단소설 『채식주의자』. 15년 만에 새로운 장정과 판형으로 출간된다. 식물화로 건설해온 극단적이며 실재적인 상상력의 강렬한 결실로 고통과 구속의 피안에 존재하는 인간의 본성에 다가간 작품." + "인터내셔널 북커상, 산클레멘테 문학상 수상작. 전세계가 주목한 인간의 역작을 다시 만나다. \n\n2016년 인터내셔널 북커상을 수상하며 한국문학의 입지를 한단계 확장시킨 한강의 명단소설 『채식주의자』. 15년 만에 새로운 장정과 판형으로 출간된다. 식물화로 건설해온 극단적이며 실재적인 상상력의 강렬한 결실로 고통과 구속의 피안에 존재하는 인간의 본성에 다가간 작품.", - coverImageRes = R.drawable.bookcover_sample, + coverImageRes = R.drawable.img_book_cover_sample, participantsCount = 210, recruitingRoomCount = 4 ), diff --git a/app/src/main/res/drawable/bookcover_sample.png b/app/src/main/res/drawable/img_book_cover_sample.png similarity index 100% rename from app/src/main/res/drawable/bookcover_sample.png rename to app/src/main/res/drawable/img_book_cover_sample.png From 79deb2e72ef598bc72a627e04015395126c68da2 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 01:11:01 +0900 Subject: [PATCH 61/68] =?UTF-8?q?[refactor]:=20MyRoomCardData=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=A8=EC=88=98=EB=A5=BC=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95=20(#6?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt index 9fa50ee4..c42f61be 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.group.done.mock -import com.texthip.thip.data.mapper.GroupDataMapper import com.texthip.thip.ui.group.myroom.mock.RoomType data class MyRoomCardData( @@ -31,5 +30,10 @@ fun MyRoomCardData.isRecruitingByType(): Boolean { } fun MyRoomCardData.getEndDateInDays(): Int { - return GroupDataMapper().extractDaysFromDeadline(endDate) + return when { + endDate.contains("일 뒤") -> { + endDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 + } + else -> 0 + } } \ No newline at end of file From 12e9c65280be9cdf0dc0c9bf85869605901d2bf3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 01:16:16 +0900 Subject: [PATCH 62/68] =?UTF-8?q?[refactor]:=20import=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B8=B0=ED=83=80=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/data/repository/BookRepository.kt | 1 - .../texthip/thip/data/repository/GroupRepository.kt | 4 +--- .../ui/group/done/viewmodel/GroupDoneViewModel.kt | 1 - .../makeroom/viewmodel/GroupMakeRoomViewModel.kt | 11 +++++++---- .../ui/group/myroom/viewmodel/GroupMyViewModel.kt | 1 - .../group/room/viewmodel/GroupRoomRecruitViewModel.kt | 1 - .../texthip/thip/ui/group/viewmodel/GroupViewModel.kt | 1 - .../thip/ui/navigator/navigations/GroupNavigation.kt | 2 -- 8 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt index 3616bd94..5641421f 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt @@ -1,7 +1,6 @@ package com.texthip.thip.data.repository import com.texthip.thip.data.model.base.handleBaseResponse -import com.texthip.thip.data.model.book.response.BookSavedResponse import com.texthip.thip.data.service.BookService import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt index c4cfd7df..d90f0e76 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt @@ -2,17 +2,15 @@ package com.texthip.thip.data.repository import android.content.Context import com.texthip.thip.R -import com.texthip.thip.data.mapper.GroupDataMapper import com.texthip.thip.data.manager.GenreManager import com.texthip.thip.data.manager.UserDataManager +import com.texthip.thip.data.mapper.GroupDataMapper import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.data.model.group.request.RoomJoinRequest import com.texthip.thip.data.model.group.response.PaginationResult import com.texthip.thip.data.service.GroupService import com.texthip.thip.ui.group.done.mock.MyRoomsPaginationResult -import com.texthip.thip.ui.group.myroom.mock.GroupCardData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index 48d308c5..7bfee0ce 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -3,7 +3,6 @@ package com.texthip.thip.ui.group.done.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.done.viewmodel.GroupDoneUiState import com.texthip.thip.ui.group.myroom.mock.RoomType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index 511274eb..045e8450 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -5,11 +5,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.R import com.texthip.thip.data.model.book.response.BookSavedResponse -import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.data.repository.BookRepository +import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.makeroom.mock.BookData -import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomUiState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow @@ -30,6 +29,10 @@ class GroupMakeRoomViewModel @Inject constructor( private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) val uiState: StateFlow = _uiState.asStateFlow() + companion object { + private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd") + } + private fun updateState(update: (GroupMakeRoomUiState) -> GroupMakeRoomUiState) { _uiState.value = update(_uiState.value) } @@ -153,8 +156,8 @@ class GroupMakeRoomViewModel @Inject constructor( category = getApiCategoryName(currentState.selectedGenreIndex), roomName = currentState.roomTitle.trim(), description = currentState.roomDescription.trim(), - progressStartDate = currentState.meetingStartDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")), - progressEndDate = currentState.meetingEndDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")), + progressStartDate = currentState.meetingStartDate.format(DATE_FORMATTER), + progressEndDate = currentState.meetingEndDate.format(DATE_FORMATTER), recruitCount = currentState.memberLimit, password = if (currentState.isPrivate) currentState.password else null, isPublic = !currentState.isPrivate diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt index f1b979e6..7bff1be9 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -3,7 +3,6 @@ package com.texthip.thip.ui.group.myroom.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyUiState import com.texthip.thip.ui.group.myroom.mock.RoomType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index 937e639a..75683851 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope import com.texthip.thip.R import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitUiState import com.texthip.thip.ui.group.room.mock.RoomAction import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index ae395813..e17618ee 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.R import com.texthip.thip.data.repository.GroupRepository -import com.texthip.thip.ui.group.viewmodel.GroupUiState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.async diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index f85de136..31ab7149 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -164,8 +164,6 @@ fun NavGraphBuilder.groupNavigation( // Group Room 화면 composable { backStackEntry -> - val route = backStackEntry.toRoute() - GroupRoomScreen( onBackClick = { navigateBack() From 4c666d151d6700871fa51514b39b09cad681fae8 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 22:20:18 +0900 Subject: [PATCH 63/68] =?UTF-8?q?[refactor]:=20PR=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/manager/GenreManager.kt | 5 +---- .../texthip/thip/data/repository/GroupRepository.kt | 13 ++----------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt index bbda20c4..2022f8c8 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt @@ -37,8 +37,5 @@ class GenreManager @Inject constructor( fun isValidGenreIndex(index: Int): Boolean { return index >= 0 && index < genres.size } - - fun getGenreByIndex(index: Int): String? { - return if (isValidGenreIndex(index)) genres[index] else null - } + } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt index d90f0e76..9ce279f1 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt @@ -53,12 +53,7 @@ class GroupRepository @Inject constructor( currentPage = joinedRoomsDto.page, nickname = joinedRoomsDto.nickname ) - } ?: PaginationResult( - data = emptyList(), - hasMore = false, - currentPage = page, - nickname = "" - ) + } } /** 카테고리별 모임방 섹션 조회 (마감임박/인기) */ @@ -106,11 +101,7 @@ class GroupRepository @Inject constructor( nextCursor = data.nextCursor, isLast = data.isLast ) - } ?: MyRoomsPaginationResult( - data = emptyList(), - nextCursor = null, - isLast = true - ) + } } /** 모집중인 모임방 상세 정보 조회 */ From 91a2b212a41c53f6d115457eb0ca9fb6b801a3d6 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 8 Aug 2025 23:11:07 +0900 Subject: [PATCH 64/68] =?UTF-8?q?[refactor]:=20=EC=9E=A5=EB=A5=B4=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=82=AD=EC=A0=9C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/mapper/GroupDataMapper.kt | 4 +-- .../thip/data/repository/GroupRepository.kt | 31 ++++++++++++------- .../component/GroupDeadlineRoomSection.kt | 12 ------- .../myroom/mock/GroupCardItemRoomData.kt | 1 - .../room/screen/GroupRoomRecruitScreen.kt | 1 - .../component/GroupFilteredSearchResult.kt | 2 -- .../search/component/GroupLiveSearchResult.kt | 5 --- .../group/search/screen/GroupSearchScreen.kt | 6 +--- .../ui/search/screen/SearchBookGroupScreen.kt | 8 ----- 9 files changed, 22 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt b/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt index c21f07db..bb14be42 100644 --- a/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt +++ b/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt @@ -37,7 +37,6 @@ class GroupDataMapper @Inject constructor() { isRecruiting = true, endDate = daysLeft, imageUrl = dto.bookImageUrl, - genreIndex = 0, isSecret = false ) } @@ -82,8 +81,7 @@ class GroupDataMapper @Inject constructor() { maxParticipants = dto.recruitCount, isRecruiting = true, endDate = extractDaysFromDeadline(dto.recruitEndDate), - imageUrl = dto.roomImageUrl, - genreIndex = 0, + imageUrl = dto.roomImageUrl ) } diff --git a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt index 9ce279f1..846a835c 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt @@ -53,7 +53,12 @@ class GroupRepository @Inject constructor( currentPage = joinedRoomsDto.page, nickname = joinedRoomsDto.nickname ) - } + } ?: PaginationResult( + data = emptyList(), + hasMore = false, + currentPage = page, + nickname = "" + ) } /** 카테고리별 모임방 섹션 조회 (마감임박/인기) */ @@ -83,7 +88,7 @@ class GroupRepository @Inject constructor( genres = genreManager.getGenres() ) ) - }.orEmpty() + } ?: emptyList() } /** 타입별 내 모임방 목록 조회 */ @@ -91,7 +96,7 @@ class GroupRepository @Inject constructor( groupService.getMyRooms(type, cursor) .handleBaseResponse() .getOrThrow() - ?.let { data -> +?.let { data -> val myRoomCards = data.roomList.map { room -> groupDataMapper.toMyRoomCardData(room) } @@ -101,25 +106,29 @@ class GroupRepository @Inject constructor( nextCursor = data.nextCursor, isLast = data.isLast ) - } + } ?: MyRoomsPaginationResult( + data = emptyList(), + nextCursor = null, + isLast = true + ) } /** 모집중인 모임방 상세 정보 조회 */ suspend fun getRoomRecruiting(roomId: Int) = runCatching { groupService.getRoomRecruiting(roomId) .handleBaseResponse() - .getOrThrow() - ?.let { data -> + .getOrThrow()!! + .let { data -> groupDataMapper.toGroupRoomData(data) - } ?: throw Exception("No recruiting data found for room $roomId") + } } /** 새 모임방 생성 */ suspend fun createRoom(request: CreateRoomRequest) = runCatching { groupService.createRoom(request) .handleBaseResponse() - .getOrThrow() - ?.roomId ?: throw Exception("Failed to create room: roomId is null") + .getOrThrow()!! + .roomId } /** 모임방 참여 또는 취소 */ @@ -127,7 +136,7 @@ class GroupRepository @Inject constructor( val request = RoomJoinRequest(type = type) groupService.joinOrCancelRoom(roomId, request) .handleBaseResponse() - .getOrThrow() - ?.type ?: throw Exception("Failed to join/cancel room: no response") + .getOrThrow()!! + .type } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt index c1c2eeac..ff9b9817 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt @@ -238,7 +238,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 30, isRecruiting = true, endDate = 3, - genreIndex = 0 ), GroupCardItemRoomData( id = 2, @@ -247,7 +246,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 20, isRecruiting = true, endDate = 2, - genreIndex = 0 ), GroupCardItemRoomData( id = 3, @@ -256,7 +254,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 30, isRecruiting = true, endDate = 3, - genreIndex = 0 ), GroupCardItemRoomData( id = 4, @@ -265,7 +262,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 30, isRecruiting = true, endDate = 3, - genreIndex = 0 ), GroupCardItemRoomData( id = 5, @@ -274,7 +270,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 20, isRecruiting = true, endDate = 1, - genreIndex = 1 ) ) @@ -287,7 +282,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 30, isRecruiting = true, endDate = 7, - genreIndex = 0 ), GroupCardItemRoomData( id = 7, @@ -296,7 +290,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 25, isRecruiting = false, endDate = 5, - genreIndex = 0 ), GroupCardItemRoomData( id = 8, @@ -305,7 +298,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 25, isRecruiting = true, endDate = 10, - genreIndex = 1 ) ) @@ -318,7 +310,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 30, isRecruiting = false, endDate = 14, - genreIndex = 0 ), GroupCardItemRoomData( id = 10, @@ -327,7 +318,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 20, isRecruiting = true, endDate = 8, - genreIndex = 2 ), GroupCardItemRoomData( id = 11, @@ -336,7 +326,6 @@ fun PreviewGroupRoomPagerSection() { maxParticipants = 20, isRecruiting = true, endDate = 12, - genreIndex = 3 ) ) @@ -383,7 +372,6 @@ fun PreviewGroupRoomPagerSectionEmptyGenre() { maxParticipants = 30, isRecruiting = true, endDate = 3, - genreIndex = 0 // 문학 장르만 ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt index 5ea7fa00..7c149e9f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt @@ -8,7 +8,6 @@ data class GroupCardItemRoomData( val isRecruiting: Boolean, val endDate: Int? = null, // 남은 일 수 val imageUrl: String? = null, // API에서 받은 이미지 URL - val genreIndex: Int, // 장르 인덱스 val isSecret: Boolean = false ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index 1fc783b1..68682f2b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -452,7 +452,6 @@ fun GroupRoomRecruitScreenPreview() { maxParticipants = 25, isRecruiting = true, endDate = 2, - genreIndex = 0 ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt index a7aaccae..72aec70c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt @@ -122,7 +122,6 @@ fun GroupFilteredSearchResultPreview() { isRecruiting = true, endDate = 7, imageUrl = null, - genreIndex = 1, isSecret = false ), GroupCardItemRoomData( id = 2, @@ -132,7 +131,6 @@ fun GroupFilteredSearchResultPreview() { isRecruiting = false, endDate = 3, imageUrl = null, - genreIndex = 1, isSecret = true ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt index 50c2f78b..e3ccac6f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt @@ -1,7 +1,6 @@ package com.texthip.thip.ui.group.search.component import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -13,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.texthip.thip.R import com.texthip.thip.ui.common.cards.CardItemRoomSmall import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.theme.ThipTheme @@ -67,7 +65,6 @@ fun GroupLiveSearchResultPreview() { isRecruiting = true, endDate = 7, imageUrl = null, - genreIndex = 0, isSecret = false ), GroupCardItemRoomData( @@ -78,7 +75,6 @@ fun GroupLiveSearchResultPreview() { isRecruiting = false, endDate = 3, imageUrl = null, - genreIndex = 1, isSecret = true ), GroupCardItemRoomData( @@ -89,7 +85,6 @@ fun GroupLiveSearchResultPreview() { isRecruiting = true, endDate = null, imageUrl = null, - genreIndex = 2, isSecret = false ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt index 55c28132..50487fbf 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt @@ -113,8 +113,7 @@ fun GroupSearchScreen( if (!isSearched) emptyList() else { val filtered = roomList.filter { room -> - (searchText.isBlank() || room.title.contains(searchText, ignoreCase = true)) && - (selectedGenreIndex == -1 || room.genreIndex == selectedGenreIndex) + (searchText.isBlank() || room.title.contains(searchText, ignoreCase = true)) } when (selectedSortOptionIndex) { 0 -> filtered.sortedBy { it.endDate } // 마감임박순 @@ -250,7 +249,6 @@ fun PreviewGroupSearchScreen() { isRecruiting = true, endDate = 3, imageUrl = null, - genreIndex = 0, isSecret = false ), GroupCardItemRoomData( @@ -261,7 +259,6 @@ fun PreviewGroupSearchScreen() { isRecruiting = true, endDate = 7, imageUrl = null, - genreIndex = 1, isSecret = true ), GroupCardItemRoomData( @@ -272,7 +269,6 @@ fun PreviewGroupSearchScreen() { isRecruiting = true, endDate = 5, imageUrl = null, - genreIndex = 2, isSecret = true ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt index d3047851..778622cc 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt @@ -151,7 +151,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 2, @@ -160,7 +159,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 3, @@ -169,7 +167,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 4, @@ -178,7 +175,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 5, @@ -187,7 +183,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 6, @@ -196,7 +191,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 7, @@ -205,7 +199,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 8, @@ -214,7 +207,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ) ) From 3bef4b6665c6a88b37931964822b567f09532474 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sat, 9 Aug 2025 17:50:04 +0900 Subject: [PATCH 65/68] =?UTF-8?q?[refactor]:=20Room=20mapper=20=EC=99=84?= =?UTF-8?q?=EC=A0=84=ED=9E=88=20=EC=82=AD=EC=A0=9C=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/data/manager/GenreManager.kt | 5 - .../thip/data/mapper/GroupDataMapper.kt | 116 --------- .../group/response/JoinedRoomListResponse.kt | 6 - .../group/response/MyRoomListResponse.kt | 2 +- .../thip/data/repository/GroupRepository.kt | 92 ++----- .../ui/group/done/screen/GroupDoneScreen.kt | 9 +- .../group/done/viewmodel/GroupDoneUiState.kt | 4 +- .../done/viewmodel/GroupDoneViewModel.kt | 24 +- .../component/GroupDeadlineRoomSection.kt | 235 ++++++------------ .../group/myroom/component/GroupMainCard.kt | 31 +-- .../ui/group/myroom/component/GroupPager.kt | 64 +++-- .../ui/group/myroom/screen/GroupMyScreen.kt | 12 +- .../group/myroom/viewmodel/GroupMyUiState.kt | 4 +- .../myroom/viewmodel/GroupMyViewModel.kt | 22 +- .../room/screen/GroupRoomRecruitScreen.kt | 66 ++--- .../room/viewmodel/GroupRoomRecruitUiState.kt | 4 +- .../viewmodel/GroupRoomRecruitViewModel.kt | 12 +- .../thip/ui/group/screen/GroupScreen.kt | 15 +- .../thip/ui/group/viewmodel/GroupUiState.kt | 10 +- .../thip/ui/group/viewmodel/GroupViewModel.kt | 24 +- .../navigator/navigations/GroupNavigation.kt | 10 +- .../java/com/texthip/thip/util/DateUtils.kt | 13 + .../java/com/texthip/thip/util/RoomUtils.kt | 18 ++ 23 files changed, 285 insertions(+), 513 deletions(-) delete mode 100644 app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt create mode 100644 app/src/main/java/com/texthip/thip/util/DateUtils.kt create mode 100644 app/src/main/java/com/texthip/thip/util/RoomUtils.kt diff --git a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt index 2022f8c8..67514a81 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt @@ -33,9 +33,4 @@ class GenreManager @Inject constructor( fun getDefaultGenre(): String { return context.getString(R.string.literature) } - - fun isValidGenreIndex(index: Int): Boolean { - return index >= 0 && index < genres.size - } - } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt b/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt deleted file mode 100644 index bb14be42..00000000 --- a/app/src/main/java/com/texthip/thip/data/mapper/GroupDataMapper.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.texthip.thip.data.mapper - -import com.texthip.thip.data.model.group.response.JoinedRoomResponse -import com.texthip.thip.data.model.group.response.MyRoomResponse -import com.texthip.thip.data.model.group.response.RecommendRoomResponse -import com.texthip.thip.data.model.group.response.RoomMainResponse -import com.texthip.thip.data.model.group.response.RoomRecruitingResponse -import com.texthip.thip.ui.group.done.mock.MyRoomCardData -import com.texthip.thip.ui.group.myroom.mock.GroupBookData -import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.myroom.mock.GroupCardData -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class GroupDataMapper @Inject constructor() { - - fun toGroupCardData(dto: JoinedRoomResponse, nickname: String): GroupCardData { - return GroupCardData( - id = dto.roomId, - title = dto.bookTitle, - members = dto.memberCount, - imageUrl = dto.bookImageUrl, - progress = dto.userPercentage, - nickname = nickname - ) - } - - fun toGroupCardItemRoomData(dto: RoomMainResponse, daysLeft: Int): GroupCardItemRoomData { - return GroupCardItemRoomData( - id = dto.roomId, - title = dto.roomName, - participants = dto.memberCount, - maxParticipants = dto.recruitCount, - isRecruiting = true, - endDate = daysLeft, - imageUrl = dto.bookImageUrl, - isSecret = false - ) - } - - fun toGroupRoomData(dto: RoomRecruitingResponse): GroupRoomData { - val bookData = GroupBookData( - title = dto.bookTitle, - author = dto.authorName, - publisher = dto.publisher, - description = dto.bookDescription, - imageUrl = dto.bookImageUrl - ) - - val recommendations = dto.recommendRooms.map { recommendDto -> - toGroupCardItemRoomDataFromRecommend(recommendDto) - } - - return GroupRoomData( - id = dto.roomId, - title = dto.roomName, - isSecret = !dto.isPublic, - description = dto.roomDescription, - startDate = dto.progressStartDate, - endDate = dto.progressEndDate, - members = dto.memberCount, - maxMembers = dto.recruitCount, - daysLeft = extractDaysFromDeadline(dto.recruitEndDate), - genre = dto.category, - bookData = bookData, - recommendations = recommendations, - buttonType = determineButtonType(dto.isHost, dto.isJoining), - roomImageUrl = dto.roomImageUrl, - bookImageUrl = dto.bookImageUrl - ) - } - - private fun toGroupCardItemRoomDataFromRecommend(dto: RecommendRoomResponse): GroupCardItemRoomData { - return GroupCardItemRoomData( - id = dto.roomId, - title = dto.roomName, - participants = dto.memberCount, - maxParticipants = dto.recruitCount, - isRecruiting = true, - endDate = extractDaysFromDeadline(dto.recruitEndDate), - imageUrl = dto.roomImageUrl - ) - } - - fun extractDaysFromDeadline(deadlineDate: String): Int { - return when { - deadlineDate.contains("일 뒤") -> { - deadlineDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 - } - else -> 0 - } - } - - fun toMyRoomCardData(room: MyRoomResponse): MyRoomCardData { - return MyRoomCardData( - roomId = room.roomId, - bookImageUrl = room.bookImageUrl, - roomName = room.roomName, - recruitCount = room.recruitCount, - memberCount = room.memberCount, - endDate = room.endDate, - type = room.type - ) - } - - private fun determineButtonType(isHost: Boolean, isJoining: Boolean): GroupBottomButtonType { - return when { - isHost -> GroupBottomButtonType.CLOSE - isJoining -> GroupBottomButtonType.CANCEL - else -> GroupBottomButtonType.JOIN - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt index b5d109c1..c5fb0eb6 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt @@ -23,9 +23,3 @@ data class JoinedRoomResponse( @SerialName("userPercentage") val userPercentage: Int ) -data class PaginationResult( - val data: List, - val hasMore: Boolean, - val currentPage: Int, - val nickname: String = "" -) diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt index b66ad0f6..aa9c6a4d 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt @@ -18,5 +18,5 @@ data class MyRoomResponse( @SerialName("recruitCount") val recruitCount: Int, @SerialName("memberCount") val memberCount: Int, @SerialName("endDate") val endDate: String, - @SerialName("type") val type: String // "playingAndRecruiting", "recruiting", "playing", "expired" + @SerialName("type") val type: String ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt index 846a835c..dbbcc89f 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt @@ -1,28 +1,23 @@ package com.texthip.thip.data.repository -import android.content.Context -import com.texthip.thip.R import com.texthip.thip.data.manager.GenreManager import com.texthip.thip.data.manager.UserDataManager -import com.texthip.thip.data.mapper.GroupDataMapper import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.data.model.group.request.RoomJoinRequest -import com.texthip.thip.data.model.group.response.PaginationResult +import com.texthip.thip.data.model.group.response.JoinedRoomListResponse +import com.texthip.thip.data.model.group.response.MyRoomListResponse +import com.texthip.thip.data.model.group.response.RoomMainList +import com.texthip.thip.data.model.group.response.RoomRecruitingResponse import com.texthip.thip.data.service.GroupService -import com.texthip.thip.ui.group.done.mock.MyRoomsPaginationResult -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton class GroupRepository @Inject constructor( private val groupService: GroupService, - private val groupDataMapper: GroupDataMapper, private val genreManager: GenreManager, - private val userDataManager: UserDataManager, - @param:ApplicationContext private val context: Context + private val userDataManager: UserDataManager ) { /** 장르 목록 조회 */ @@ -36,95 +31,44 @@ class GroupRepository @Inject constructor( } /** 내가 참여 중인 모임방 목록 조회 */ - suspend fun getMyJoinedRooms(page: Int) = runCatching { - groupService.getJoinedRooms(page) + suspend fun getMyJoinedRooms(page: Int): Result = runCatching { + val response = groupService.getJoinedRooms(page) .handleBaseResponse() .getOrThrow() - ?.let { joinedRoomsDto -> - userDataManager.cacheUserName(joinedRoomsDto.nickname) - - val groups = joinedRoomsDto.roomList.map { dto -> - groupDataMapper.toGroupCardData(dto, joinedRoomsDto.nickname) - } - - PaginationResult( - data = groups, - hasMore = !joinedRoomsDto.last, - currentPage = joinedRoomsDto.page, - nickname = joinedRoomsDto.nickname - ) - } ?: PaginationResult( - data = emptyList(), - hasMore = false, - currentPage = page, - nickname = "" - ) + + response?.let { joinedRoomsDto -> + userDataManager.cacheUserName(joinedRoomsDto.nickname) + } + + response } /** 카테고리별 모임방 섹션 조회 (마감임박/인기) */ - suspend fun getRoomSections(category: String = "") = runCatching { + suspend fun getRoomSections(category: String = ""): Result = runCatching { val finalCategory = category.ifEmpty { genreManager.getDefaultGenre() } val apiCategory = genreManager.mapGenreToApiCategory(finalCategory) groupService.getRooms(apiCategory) .handleBaseResponse() .getOrThrow() - ?.let { roomsData -> - listOf( - GroupRoomSectionData( - title = context.getString(R.string.room_section_deadline), - rooms = roomsData.deadlineRoomList.map { dto -> - val daysLeft = groupDataMapper.extractDaysFromDeadline(dto.deadlineDate) - groupDataMapper.toGroupCardItemRoomData(dto, daysLeft) - }, - genres = genreManager.getGenres() - ), - GroupRoomSectionData( - title = context.getString(R.string.room_section_popular), - rooms = roomsData.popularRoomList.map { dto -> - val daysLeft = groupDataMapper.extractDaysFromDeadline(dto.deadlineDate) - groupDataMapper.toGroupCardItemRoomData(dto, daysLeft) - }, - genres = genreManager.getGenres() - ) - ) - } ?: emptyList() } /** 타입별 내 모임방 목록 조회 */ - suspend fun getMyRoomsByType(type: String?, cursor: String? = null) = runCatching { + suspend fun getMyRoomsByType(type: String?, cursor: String? = null): Result = runCatching { groupService.getMyRooms(type, cursor) .handleBaseResponse() .getOrThrow() -?.let { data -> - val myRoomCards = data.roomList.map { room -> - groupDataMapper.toMyRoomCardData(room) - } - - MyRoomsPaginationResult( - data = myRoomCards, - nextCursor = data.nextCursor, - isLast = data.isLast - ) - } ?: MyRoomsPaginationResult( - data = emptyList(), - nextCursor = null, - isLast = true - ) } /** 모집중인 모임방 상세 정보 조회 */ - suspend fun getRoomRecruiting(roomId: Int) = runCatching { + suspend fun getRoomRecruiting(roomId: Int): Result = runCatching { groupService.getRoomRecruiting(roomId) .handleBaseResponse() .getOrThrow()!! - .let { data -> - groupDataMapper.toGroupRoomData(data) - } } /** 새 모임방 생성 */ - suspend fun createRoom(request: CreateRoomRequest) = runCatching { + suspend fun createRoom(request: CreateRoomRequest): Result = runCatching { groupService.createRoom(request) .handleBaseResponse() .getOrThrow()!! @@ -132,7 +76,7 @@ class GroupRepository @Inject constructor( } /** 모임방 참여 또는 취소 */ - suspend fun joinOrCancelRoom(roomId: Int, type: String) = runCatching { + suspend fun joinOrCancelRoom(roomId: Int, type: String): Result = runCatching { val request = RoomJoinRequest(type = type) groupService.joinOrCancelRoom(roomId, request) .handleBaseResponse() diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt index 9cbd24c0..94e7121d 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt @@ -24,13 +24,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.MyRoomResponse import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar -import com.texthip.thip.ui.group.done.mock.isRecruitingByType import com.texthip.thip.ui.group.done.viewmodel.GroupDoneViewModel 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.util.RoomUtils @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -97,7 +98,7 @@ fun GroupDoneScreen( imageUrl = room.bookImageUrl, participants = room.memberCount, maxParticipants = room.recruitCount, // 모집 인원 수 사용 - isRecruiting = room.isRecruitingByType(), + isRecruiting = RoomUtils.isRecruitingByType(room.type), onClick = { /* 완료된 모임방은 클릭 불가 */ } ) } @@ -108,7 +109,6 @@ fun GroupDoneScreen( } - @Preview @Composable fun GroupDoneScreenPreview() { @@ -116,4 +116,5 @@ fun GroupDoneScreenPreview() { // Preview에서는 ViewModel을 사용할 수 없으므로 기본 레이아웃만 표시 GroupDoneScreen() } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt index 47e32828..8af6d4bc 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt @@ -1,9 +1,9 @@ package com.texthip.thip.ui.group.done.viewmodel -import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import com.texthip.thip.data.model.group.response.MyRoomResponse data class GroupDoneUiState( - val expiredRooms: List = emptyList(), + val expiredRooms: List = emptyList(), val isLoading: Boolean = false, val isLoadingMore: Boolean = false, val hasMore: Boolean = true, diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt index 7bfee0ce..f76b7678 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -65,18 +65,20 @@ class GroupDoneViewModel @Inject constructor( } repository.getMyRoomsByType(RoomType.EXPIRED.value, nextCursor) - .onSuccess { paginationResult -> - val currentList = if (reset) emptyList() else uiState.value.expiredRooms - updateState { - it.copy( - expiredRooms = currentList + paginationResult.data, - error = null, - isLoadingMore = false, - hasMore = !paginationResult.isLast - ) + .onSuccess { myRoomListResponse -> + myRoomListResponse?.let { response -> + val currentList = if (reset) emptyList() else uiState.value.expiredRooms + updateState { + it.copy( + expiredRooms = currentList + response.roomList, + error = null, + isLoadingMore = false, + hasMore = !response.isLast + ) + } + nextCursor = response.nextCursor + isLastPage = response.isLast } - nextCursor = paginationResult.nextCursor - isLastPage = paginationResult.isLast } .onFailure { exception -> updateState { it.copy(error = exception.message) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt index ff9b9817..eb293739 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt @@ -29,20 +29,21 @@ import androidx.compose.ui.unit.dp import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.GenreChipRow import com.texthip.thip.ui.common.cards.CardItemRoom -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData +import com.texthip.thip.data.model.group.response.RoomMainList +import com.texthip.thip.data.model.group.response.RoomMainResponse 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.util.DateUtils @SuppressLint("UnusedBoxWithConstraintsScope") @Composable fun GroupRoomDeadlineSection( - roomSections: List, + roomMainList: RoomMainList?, selectedGenreIndex: Int, errorMessage: String? = null, onGenreSelect: (Int) -> Unit, - onRoomClick: (GroupCardItemRoomData) -> Unit + onRoomClick: (RoomMainResponse) -> Unit ) { val sideMargin = 30.dp @@ -63,27 +64,24 @@ fun GroupRoomDeadlineSection( val pageSpacing = (-(cardWidth - (cardWidth * scale)) / 2) + desiredGap - // 데이터가 없어도 기본 구조 표시 - val effectiveRoomSections = roomSections.ifEmpty { - // 기본 구조를 위한 더미 섹션 생성 - listOf( - GroupRoomSectionData( - title = stringResource(R.string.room_section_deadline), - rooms = emptyList(), - genres = listOf( - stringResource(R.string.literature), - stringResource(R.string.science_it), - stringResource(R.string.social_science), - stringResource(R.string.humanities), - stringResource(R.string.art) - ) - ) - ) - } + // 기본 장르 목록 + val defaultGenres = listOf( + stringResource(R.string.literature), + stringResource(R.string.science_it), + stringResource(R.string.social_science), + stringResource(R.string.humanities), + stringResource(R.string.art) + ) + + // 마감 임박 방 목록과 인기 방 목록을 섹션으로 구성 + val roomSections = listOf( + Pair(stringResource(R.string.room_section_deadline), roomMainList?.deadlineRoomList ?: emptyList()), + Pair(stringResource(R.string.room_section_popular), roomMainList?.popularRoomList ?: emptyList()) + ) val effectivePagerState = rememberPagerState( initialPage = 0, - pageCount = { effectiveRoomSections.size } + pageCount = { roomSections.size } ) HorizontalPager( @@ -92,7 +90,7 @@ fun GroupRoomDeadlineSection( pageSpacing = pageSpacing, modifier = Modifier.fillMaxWidth() ) { page -> - val section = effectiveRoomSections[page] + val (sectionTitle, rooms) = roomSections[page] val isCurrent = effectivePagerState.currentPage == page val scale = if (isCurrent) 1f else 0.94f @@ -120,14 +118,14 @@ fun GroupRoomDeadlineSection( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = section.title, + text = sectionTitle, style = typography.title_b700_s20_h24, color = colors.White ) Spacer(Modifier.height(40.dp)) GenreChipRow( - genres = section.genres, + genres = defaultGenres, selectedIndex = selectedGenreIndex, onSelect = onGenreSelect ) @@ -164,7 +162,7 @@ fun GroupRoomDeadlineSection( } } // 데이터 없음 상태 - section.rooms.isEmpty() -> { + rooms.isEmpty() -> { Column( modifier = Modifier .padding(top = 30.dp) @@ -192,21 +190,23 @@ fun GroupRoomDeadlineSection( verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.fillMaxWidth() ) { - section.rooms.forEach { room -> + rooms.forEach { room -> + // RoomMainResponse를 CardItemRoom에 맞게 변환 + val daysLeft = DateUtils.extractDaysFromDeadline(room.deadlineDate) CardItemRoom( - title = room.title, - participants = room.participants, - maxParticipants = room.maxParticipants, - isRecruiting = room.isRecruiting, - endDate = room.endDate, - imageUrl = room.imageUrl, + title = room.roomName, + participants = room.memberCount, + maxParticipants = room.recruitCount, + isRecruiting = true, // RoomMainResponse에는 모집중인 방만 있음 + endDate = daysLeft, + imageUrl = room.bookImageUrl, onClick = { onRoomClick(room) }, hasBorder = true, ) } } - if (section.rooms.size < 4) { + if (rooms.size < 4) { Spacer( modifier = Modifier .weight(1f, fill = true) @@ -223,132 +223,49 @@ fun GroupRoomDeadlineSection( } } -@Preview() + +@Preview @Composable fun PreviewGroupRoomPagerSection() { ThipTheme { - val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") - - // 마감 임박한 독서 모임방 + // RoomMainResponse 형태의 더미 데이터 val deadlineRooms = listOf( - GroupCardItemRoomData( - id = 1, - title = "시집만 읽는 사람들 3월", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, + RoomMainResponse( + roomId = 1, + roomName = "시집만 읽는 사람들 3월", + memberCount = 22, + recruitCount = 30, + deadlineDate = "3일 뒤", + bookImageUrl = "https://picsum.photos/300/200?1" ), - GroupCardItemRoomData( - id = 2, - title = "일본 소설 좋아하는 사람들", - participants = 15, - maxParticipants = 20, - isRecruiting = true, - endDate = 2, - ), - GroupCardItemRoomData( - id = 3, - title = "명작 같이 읽기방", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - ), - GroupCardItemRoomData( - id = 4, - title = "명작 같이 읽기방", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - ), - GroupCardItemRoomData( - id = 5, - title = "물리책 읽는 방", - participants = 13, - maxParticipants = 20, - isRecruiting = true, - endDate = 1, + RoomMainResponse( + roomId = 2, + roomName = "일본 소설 좋아하는 사람들", + memberCount = 15, + recruitCount = 20, + deadlineDate = "2일 뒤", + bookImageUrl = "https://picsum.photos/300/200?2" ) ) - // 인기 있는 독서 모임방 val popularRooms = listOf( - GroupCardItemRoomData( - id = 6, - title = "베스트셀러 토론방", - participants = 28, - maxParticipants = 30, - isRecruiting = true, - endDate = 7, - ), - GroupCardItemRoomData( - id = 7, - title = "인기 소설 완독방", - participants = 25, - maxParticipants = 25, - isRecruiting = false, - endDate = 5, - ), - GroupCardItemRoomData( - id = 8, - title = "트렌드 과학서 읽기", - participants = 20, - maxParticipants = 25, - isRecruiting = true, - endDate = 10, + RoomMainResponse( + roomId = 6, + roomName = "베스트셀러 토론방", + memberCount = 28, + recruitCount = 30, + deadlineDate = "7일 뒤", + bookImageUrl = "https://picsum.photos/300/200?6" ) ) - // 인플루언서, 작가 독서 모임방 - val influencerRooms = listOf( - GroupCardItemRoomData( - id = 9, - title = "작가와 함께하는 독서방", - participants = 30, - maxParticipants = 30, - isRecruiting = false, - endDate = 14, - ), - GroupCardItemRoomData( - id = 10, - title = "유명 북튜버와 읽기", - participants = 18, - maxParticipants = 20, - isRecruiting = true, - endDate = 8, - ), - GroupCardItemRoomData( - id = 11, - title = "작가 초청 인문학방", - participants = 15, - maxParticipants = 20, - isRecruiting = true, - endDate = 12, - ) - ) - - val roomSections = listOf( - GroupRoomSectionData( - title = stringResource(R.string.deadline_string), - rooms = deadlineRooms, - genres = genres - ), - GroupRoomSectionData( - title = "인기 있는 독서 모임방", - rooms = popularRooms, - genres = genres - ), - GroupRoomSectionData( - title = "인플루언서·작가 독서 모임방", - rooms = influencerRooms, - genres = genres - ) + val roomMainList = RoomMainList( + deadlineRoomList = deadlineRooms, + popularRoomList = emptyList() ) GroupRoomDeadlineSection( - roomSections = roomSections, + roomMainList = roomMainList, selectedGenreIndex = 0, errorMessage = null, onGenreSelect = {}, @@ -361,30 +278,24 @@ fun PreviewGroupRoomPagerSection() { @Composable fun PreviewGroupRoomPagerSectionEmptyGenre() { ThipTheme { - val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") - - // 특정 장르에만 데이터가 있는 경우 (문학 장르만 데이터 존재) val deadlineRooms = listOf( - GroupCardItemRoomData( - id = 12, - title = "시집만 읽는 사람들 3월", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, + RoomMainResponse( + roomId = 12, + roomName = "시집만 읽는 사람들 3월", + memberCount = 22, + recruitCount = 30, + deadlineDate = "3일 뒤", + bookImageUrl = "https://picsum.photos/300/200?12" ) ) - val roomSections = listOf( - GroupRoomSectionData( - title = "마감 임박한 독서 모임방", - rooms = deadlineRooms, - genres = genres - ) + val roomMainList = RoomMainList( + deadlineRoomList = deadlineRooms, + popularRoomList = emptyList() ) GroupRoomDeadlineSection( - roomSections = roomSections, + roomMainList = roomMainList, selectedGenreIndex = 0, errorMessage = null, onGenreSelect = {}, diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt index 6b857291..0a531efc 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt @@ -32,14 +32,15 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.texthip.thip.R -import com.texthip.thip.ui.group.myroom.mock.GroupCardData +import com.texthip.thip.data.model.group.response.JoinedRoomResponse 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 GroupMainCard( - data: GroupCardData, + data: JoinedRoomResponse, + userName: String = "", backgroundColor: Color = Color.White, onClick: () -> Unit = {} ) { @@ -75,7 +76,7 @@ fun GroupMainCard( ) { // 책 이미지 AsyncImage( - model = data.imageUrl ?: R.drawable.img_book_cover_sample, + model = data.bookImageUrl ?: R.drawable.img_book_cover_sample, contentDescription = "책 이미지", modifier = Modifier .size(width = 80.dp, height = 107.dp), @@ -89,7 +90,7 @@ fun GroupMainCard( Spacer(Modifier.height(2.dp)) // 제목 Text( - text = data.title, + text = data.bookTitle, style = typography.smalltitle_sb600_s18_h24, color = colors.Black, maxLines = 1 @@ -108,7 +109,7 @@ fun GroupMainCard( verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(R.string.group_participant, data.members), + text = stringResource(R.string.group_participant, data.memberCount), color = colors.Grey02, style = typography.menu_sb600_s12, ) @@ -123,13 +124,13 @@ fun GroupMainCard( // 닉네임 + 진행도 Row(verticalAlignment = Alignment.Bottom) { Text( - text = stringResource(R.string.group_progress, data.nickname), + text = stringResource(R.string.group_progress, userName), color = colors.Grey02, style = typography.view_m500_s14 ) Spacer(Modifier.width(4.dp)) Text( - text = "${data.progress}", + text = "${data.userPercentage}", color = colors.Purple, style = typography.smalltitle_sb600_s16_h20 ) @@ -141,7 +142,7 @@ fun GroupMainCard( } Spacer(Modifier.height(10.dp)) - val percentage = data.progress.toFloat() + val percentage = data.userPercentage.toFloat() Box( modifier = Modifier .fillMaxWidth() @@ -168,14 +169,14 @@ fun GroupMainCard( fun PreviewMyGroupMainCard() { ThipTheme { GroupMainCard( - data = GroupCardData( - id = 1, - title = "호르몬 체인지 완독하는 방", - members = 22, - imageUrl = "https://picsum.photos/300/200?1", - progress = 40, - nickname = "uibowl1" + data = JoinedRoomResponse( + roomId = 1, + bookTitle = "호르몬 체인지 완독하는 방", + memberCount = 22, + bookImageUrl = "https://picsum.photos/300/200?1", + userPercentage = 40 ), + userName = "uibowl1님", onClick = {} ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt index 01e2164d..8e3c2d42 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt @@ -16,16 +16,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.texthip.thip.R -import com.texthip.thip.ui.group.myroom.mock.GroupCardData +import com.texthip.thip.data.model.group.response.JoinedRoomResponse import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @SuppressLint("UnusedBoxWithConstraintsScope") @Composable fun GroupPager( - groupCards: List, - onCardClick: (GroupCardData) -> Unit, + groupCards: List, + userName: String = "", + onCardClick: (JoinedRoomResponse) -> Unit, onCardVisible: ((Int) -> Unit)? = null ) { val scale = 0.86f @@ -57,6 +57,7 @@ fun GroupPager( ) { GroupMainCard( data = groupCards[0], + userName = userName, onClick = { onCardClick(groupCards[0]) }, backgroundColor = colors.White ) @@ -113,6 +114,7 @@ fun GroupPager( ) { GroupMainCard( data = groupCards[actualIndex], + userName = userName, onClick = { onCardClick(groupCards[actualIndex]) }, backgroundColor = bgColor ) @@ -139,29 +141,26 @@ fun GroupPager( fun PreviewMyGroupPager() { ThipTheme { val list = listOf( - GroupCardData( - id = 1, - title = "호르몬 체인지 완독하는 방", - members = 22, - imageUrl = "https://picsum.photos/300/200?1", - progress = 40, - nickname = "uibowl1님" + JoinedRoomResponse( + roomId = 1, + bookTitle = "호르몬 체인지 완독하는 방", + memberCount = 22, + bookImageUrl = "https://picsum.photos/300/200?1", + userPercentage = 40 ), - GroupCardData( - id = 2, - title = "명작 읽기방", - members = 10, - imageUrl = "https://picsum.photos/300/200?2", - progress = 70, - nickname = "joyce님" + JoinedRoomResponse( + roomId = 2, + bookTitle = "명작 읽기방", + memberCount = 10, + bookImageUrl = "https://picsum.photos/300/200?2", + userPercentage = 70 ), - GroupCardData( - id = 3, - title = "또 다른 방", - members = 13, - imageUrl = "https://picsum.photos/300/200?3", - progress = 10, - nickname = "other님" + JoinedRoomResponse( + roomId = 3, + bookTitle = "또 다른 방", + memberCount = 13, + bookImageUrl = "https://picsum.photos/300/200?3", + userPercentage = 10 ) ) GroupPager(groupCards = list, onCardClick = {}) @@ -173,16 +172,15 @@ fun PreviewMyGroupPager() { fun PreviewSingleGroupPager() { ThipTheme { val single = listOf( - GroupCardData( - id = 4, - title = "단일 그룹", - members = 15, - imageUrl = "https://picsum.photos/300/200?4", - progress = 60, - nickname = "single님" + JoinedRoomResponse( + roomId = 4, + bookTitle = "단일 그룹", + memberCount = 15, + bookImageUrl = "https://picsum.photos/300/200?4", + userPercentage = 60 ) ) - GroupPager(groupCards = single, onCardClick = {}) + GroupPager(groupCards = single, onCardClick = {}, userName = "규빈") } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index 31f4bd64..36787848 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -26,21 +26,20 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.MyRoomResponse import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar -import com.texthip.thip.ui.group.done.mock.MyRoomCardData -import com.texthip.thip.ui.group.done.mock.getEndDateInDays -import com.texthip.thip.ui.group.done.mock.isRecruitingByType import com.texthip.thip.ui.group.myroom.component.GroupMyRoomFilterRow import com.texthip.thip.ui.group.myroom.mock.RoomType import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.util.RoomUtils @OptIn(ExperimentalMaterial3Api::class) @Composable fun GroupMyScreen( - onCardClick: (MyRoomCardData) -> Unit = {}, + onCardClick: (MyRoomResponse) -> Unit = {}, onNavigateBack: () -> Unit = {}, viewModel: GroupMyViewModel = hiltViewModel() ) { @@ -135,8 +134,8 @@ fun GroupMyScreen( title = room.roomName, participants = room.memberCount, maxParticipants = room.recruitCount, - isRecruiting = room.isRecruitingByType(), - endDate = room.getEndDateInDays(), + isRecruiting = RoomUtils.isRecruitingByType(room.type), + endDate = RoomUtils.getEndDateInDays(room.endDate), imageUrl = room.bookImageUrl, onClick = { onCardClick(room) } ) @@ -168,3 +167,4 @@ fun GroupMyScreen( } } + diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt index 937df8f8..65f5a5b2 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt @@ -1,10 +1,10 @@ package com.texthip.thip.ui.group.myroom.viewmodel -import com.texthip.thip.ui.group.done.mock.MyRoomCardData +import com.texthip.thip.data.model.group.response.MyRoomResponse import com.texthip.thip.ui.group.myroom.mock.RoomType data class GroupMyUiState( - val myRooms: List = emptyList(), + val myRooms: List = emptyList(), val currentRoomType: RoomType = RoomType.PLAYING_AND_RECRUITING, val isLoading: Boolean = false, val isLoadingMore: Boolean = false, diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt index 7bff1be9..6b8d79df 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -48,17 +48,19 @@ class GroupMyViewModel @Inject constructor( } repository.getMyRoomsByType(uiState.value.currentRoomType.value, nextCursor) - .onSuccess { paginationResult -> - val currentList = if (reset) emptyList() else uiState.value.myRooms - updateState { - it.copy( - myRooms = currentList + paginationResult.data, - error = null, - hasMore = !paginationResult.isLast - ) + .onSuccess { myRoomListResponse -> + myRoomListResponse?.let { response -> + val currentList = if (reset) emptyList() else uiState.value.myRooms + updateState { + it.copy( + myRooms = currentList + response.roomList, + error = null, + hasMore = !response.isLast + ) + } + nextCursor = response.nextCursor + isLastPage = response.isLast } - nextCursor = paginationResult.nextCursor - isLastPage = paginationResult.isLast } .onFailure { exception -> updateState { it.copy(error = exception.message) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index 68682f2b..4195baf0 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.RecommendRoomResponse import com.texthip.thip.ui.common.cards.CardItemRoomSmall import com.texthip.thip.ui.common.cards.CardRoomBook import com.texthip.thip.ui.common.modal.DialogPopup @@ -49,14 +50,14 @@ import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitViewModel 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.util.DateUtils import kotlinx.coroutines.delay @Composable fun GroupRoomRecruitScreen( roomId: Int, viewModel: GroupRoomRecruitViewModel = hiltViewModel(), - mockRoomDetail: GroupRoomData? = null, // Preview용 mock 데이터 - onRecommendationClick: (GroupCardItemRoomData) -> Unit = {}, + onRecommendationClick: (RecommendRoomResponse) -> Unit = {}, onNavigateToGroupScreen: (String) -> Unit = {}, // GroupScreen으로 네비게이션 + 토스트 메시지 onBackClick: () -> Unit = {} // 뒤로가기 ) { @@ -66,9 +67,7 @@ fun GroupRoomRecruitScreen( // 데이터 로딩 LaunchedEffect(roomId) { - if (mockRoomDetail == null) { - viewModel.loadRoomDetail(roomId) - } + viewModel.loadRoomDetail(roomId) } // GroupScreen으로 네비게이션 @@ -91,8 +90,8 @@ fun GroupRoomRecruitScreen( return@Box } - // 데이터가 없는 경우 (Preview에서는 mock 데이터 사용) - val detail = uiState.roomDetail ?: mockRoomDetail ?: return@Box + // 데이터가 없는 경우 + val detail = uiState.roomDetail ?: return@Box Image( painter = painterResource(id = R.drawable.group_room_recruiting), @@ -143,11 +142,11 @@ fun GroupRoomRecruitScreen( verticalAlignment = Alignment.CenterVertically ) { Text( - text = detail.title, + text = detail.roomName, style = typography.bigtitle_b700_s22_h24, color = colors.White ) - if (detail.isSecret) { + if (!detail.isPublic) { Spacer(Modifier.width(2.dp)) Icon( painter = painterResource(id = R.drawable.ic_lock), @@ -172,7 +171,7 @@ fun GroupRoomRecruitScreen( ) Text( - text = detail.description, + text = detail.roomDescription, style = typography.copy_r400_s12_h20, color = colors.Grey, modifier = Modifier @@ -206,8 +205,8 @@ fun GroupRoomRecruitScreen( modifier = Modifier.padding(top = 12.dp), text = stringResource( R.string.group_room_period, - detail.startDate, - detail.endDate + detail.progressStartDate, + detail.progressEndDate ), style = typography.timedate_r400_s11, color = colors.Grey @@ -241,7 +240,7 @@ fun GroupRoomRecruitScreen( Text( text = stringResource( R.string.group_room_screen_participant_count, - detail.members + detail.memberCount ), style = typography.menu_sb600_s12, color = colors.White @@ -250,7 +249,7 @@ fun GroupRoomRecruitScreen( Text( text = stringResource( R.string.group_room_screen_participant_count_max, - detail.maxMembers + detail.recruitCount ), style = typography.info_m500_s12, color = colors.Grey @@ -277,10 +276,12 @@ fun GroupRoomRecruitScreen( color = colors.White ) Spacer(Modifier.width(4.dp)) + // recruitEndDate에서 남은 일수 추출 + val daysLeft = DateUtils.extractDaysFromDeadline(detail.recruitEndDate) Text( text = stringResource( R.string.group_room_screen_end_date, - detail.daysLeft + daysLeft ), style = typography.info_m500_s12, color = colors.NeonGreen @@ -301,7 +302,7 @@ fun GroupRoomRecruitScreen( ) Spacer(Modifier.width(4.dp)) Text( - text = detail.genre, + text = detail.category, style = typography.info_m500_s12, color = colors.genreColor ) @@ -311,15 +312,15 @@ fun GroupRoomRecruitScreen( //읽을 책 정보 CardRoomBook( - title = detail.bookData.title, - author = detail.bookData.author, - publisher = detail.bookData.publisher, - description = detail.bookData.description, - imageUrl = detail.bookData.imageUrl + title = detail.bookTitle, + author = detail.authorName, + publisher = detail.publisher, + description = detail.bookDescription, + imageUrl = detail.bookImageUrl ) // 추천 모임방이 있을 때만 표시 - if (detail.recommendations.isNotEmpty()) { + if (detail.recommendRooms.isNotEmpty()) { Text( modifier = Modifier.padding(top = 40.dp), text = stringResource(R.string.group_recommend), @@ -334,13 +335,15 @@ fun GroupRoomRecruitScreen( .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - items(detail.recommendations) { rec -> + items(detail.recommendRooms) { rec -> + // RecommendRoomResponse에서 데이터 추출 + val daysLeft = DateUtils.extractDaysFromDeadline(rec.recruitEndDate) CardItemRoomSmall( - title = rec.title, - participants = rec.participants, - maxParticipants = rec.maxParticipants, - endDate = rec.endDate, - imageUrl = rec.imageUrl, + title = rec.roomName, + participants = rec.memberCount, + maxParticipants = rec.recruitCount, + endDate = daysLeft, + imageUrl = rec.roomImageUrl, onClick = { onRecommendationClick(rec) } ) } @@ -350,8 +353,8 @@ fun GroupRoomRecruitScreen( } } - // 하단 버튼 (Preview에서는 mockRoomDetail의 buttonType 사용) - val buttonType = uiState.currentButtonType ?: mockRoomDetail?.buttonType + // 하단 버튼 + val buttonType = uiState.currentButtonType if (buttonType != null) { val buttonText = when (buttonType) { GroupBottomButtonType.JOIN -> stringResource(R.string.group_room_screen_participant) @@ -481,10 +484,9 @@ fun GroupRoomRecruitScreenPreview() { GroupRoomRecruitScreen( roomId = 1, - mockRoomDetail = detailJoin, onRecommendationClick = {}, onNavigateToGroupScreen = {}, onBackClick = {} ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt index 1f7fa194..8c3bd88b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt @@ -1,10 +1,10 @@ package com.texthip.thip.ui.group.room.viewmodel import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import com.texthip.thip.data.model.group.response.RoomRecruitingResponse data class GroupRoomRecruitUiState( - val roomDetail: GroupRoomData? = null, + val roomDetail: RoomRecruitingResponse? = null, val isLoading: Boolean = false, val currentButtonType: GroupBottomButtonType? = null, val showToast: Boolean = false, diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt index 75683851..9012d0ba 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -36,10 +36,16 @@ class GroupRoomRecruitViewModel @Inject constructor( repository.getRoomRecruiting(roomId) .onSuccess { data -> + // RoomRecruitingResponse에서 buttonType 결정 + val buttonType = when { + data.isHost -> GroupBottomButtonType.CLOSE + data.isJoining -> GroupBottomButtonType.CANCEL + else -> GroupBottomButtonType.JOIN + } updateState { it.copy( roomDetail = data, - currentButtonType = data.buttonType, + currentButtonType = buttonType, isLoading = false ) } @@ -52,7 +58,7 @@ class GroupRoomRecruitViewModel @Inject constructor( fun onParticipationClick() { viewModelScope.launch { - val roomId = uiState.value.roomDetail?.id ?: return@launch + val roomId = uiState.value.roomDetail?.roomId ?: return@launch repository.joinOrCancelRoom(roomId, RoomAction.JOIN.value) .onSuccess { @@ -75,7 +81,7 @@ class GroupRoomRecruitViewModel @Inject constructor( } pendingAction = { viewModelScope.launch { - val roomId = uiState.value.roomDetail?.id ?: return@launch + val roomId = uiState.value.roomDetail?.roomId ?: return@launch repository.joinOrCancelRoom(roomId, RoomAction.CANCEL.value) .onSuccess { diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index 6f5a3567..9bbfe855 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -91,9 +91,10 @@ fun GroupScreen( Spacer(Modifier.height(20.dp)) GroupPager( - groupCards = uiState.myGroups, - onCardClick = { groupCard -> - onNavigateToGroupRoom(groupCard.id) + groupCards = uiState.myJoinedRooms, + userName = uiState.userName, + onCardClick = { joinedRoom -> + onNavigateToGroupRoom(joinedRoom.roomId) }, onCardVisible = { cardIndex -> viewModel.onCardVisible(cardIndex) @@ -111,18 +112,14 @@ fun GroupScreen( // 마감 임박한 독서 모임방 GroupRoomDeadlineSection( - roomSections = uiState.roomSections, + roomMainList = uiState.roomMainList, selectedGenreIndex = uiState.selectedGenreIndex, errorMessage = uiState.roomSectionsError, onGenreSelect = { genreIndex -> viewModel.selectGenre(genreIndex) }, onRoomClick = { room -> - if (room.isRecruiting) { - onNavigateToGroupRecruit(room.id) - } else { - onNavigateToGroupRoom(room.id) - } + onNavigateToGroupRecruit(room.roomId) } ) Spacer(Modifier.height(102.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt index a5f7d6cf..82f5ab41 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt @@ -1,20 +1,20 @@ package com.texthip.thip.ui.group.viewmodel -import com.texthip.thip.ui.group.myroom.mock.GroupCardData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData +import com.texthip.thip.data.model.group.response.JoinedRoomResponse +import com.texthip.thip.data.model.group.response.RoomMainList data class GroupUiState( - val myGroups: List = emptyList(), + val myJoinedRooms: List = emptyList(), val hasMoreMyGroups: Boolean = true, val isRefreshing: Boolean = false, val isLoadingMoreMyGroups: Boolean = false, - val roomSections: List = emptyList(), + val roomMainList: RoomMainList? = null, val roomSectionsError: String? = null, val userName: String = "", val selectedGenreIndex: Int = 0, val showToast: Boolean = false, val toastMessage: String = "" ) { - val hasContent: Boolean get() = myGroups.isNotEmpty() || roomSections.isNotEmpty() + val hasContent: Boolean get() = myJoinedRooms.isNotEmpty() || (roomMainList != null) val canLoadMore: Boolean get() = hasMoreMyGroups && !isRefreshing && !isLoadingMoreMyGroups } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index e17618ee..05d3d3b4 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -79,15 +79,17 @@ class GroupViewModel @Inject constructor( if (!uiState.value.hasMoreMyGroups) break repository.getMyJoinedRooms(page) - .onSuccess { paginationResult -> - updateState { - it.copy( - myGroups = it.myGroups + paginationResult.data, - hasMoreMyGroups = paginationResult.hasMore - ) + .onSuccess { joinedRoomsResponse -> + joinedRoomsResponse?.let { response -> + updateState { + it.copy( + myJoinedRooms = it.myJoinedRooms + response.roomList, + hasMoreMyGroups = !response.last + ) + } + loadedPagesCount++ + currentMyGroupsPage = page + 1 } - loadedPagesCount++ - currentMyGroupsPage = page + 1 } .onFailure { break @@ -131,8 +133,8 @@ class GroupViewModel @Inject constructor( } repository.getRoomSections(selectedGenre) - .onSuccess { sections -> - updateState { it.copy(roomSections = sections) } + .onSuccess { roomMainList -> + updateState { it.copy(roomMainList = roomMainList) } } .onFailure { error -> updateState { it.copy(roomSectionsError = error.message) } @@ -178,7 +180,7 @@ class GroupViewModel @Inject constructor( loadedPagesCount = 0 updateState { it.copy( - myGroups = emptyList(), + myJoinedRooms = emptyList(), hasMoreMyGroups = true ) } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 31ab7149..247a7446 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -126,15 +126,17 @@ fun NavGraphBuilder.groupNavigation( val uiState by groupViewModel.uiState.collectAsState() GroupSearchScreen( - roomList = uiState.roomSections.flatMap { it.rooms }, //임시 + roomList = emptyList(), //TODO: RoomMainResponse -> GroupCardItemRoomData 변환 필요 onNavigateBack = { navigateBack() }, onRoomClick = { room -> if (room.isRecruiting) { - navController.navigateToGroupRecruit(room.id) + // TODO: GroupCardItemRoomData -> RoomMainResponse 변환 후 roomId 사용 + // navController.navigateToGroupRecruit(room.roomId) } else { - navController.navigateToGroupRoom(room.id) + // TODO: GroupCardItemRoomData -> RoomMainResponse 변환 후 roomId 사용 + // navController.navigateToGroupRoom(room.roomId) } } ) @@ -148,7 +150,7 @@ fun NavGraphBuilder.groupNavigation( GroupRoomRecruitScreen( roomId = roomId, onRecommendationClick = { recommendation -> - navController.navigateToRecommendedGroupRecruit(recommendation.id) + navController.navigateToRecommendedGroupRecruit(recommendation.roomId) }, onNavigateToGroupScreen = { toastMessage -> // GroupScreen에 토스트 메시지 전달 diff --git a/app/src/main/java/com/texthip/thip/util/DateUtils.kt b/app/src/main/java/com/texthip/thip/util/DateUtils.kt new file mode 100644 index 00000000..5f58e639 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/util/DateUtils.kt @@ -0,0 +1,13 @@ +package com.texthip.thip.util + + +object DateUtils { + fun extractDaysFromDeadline(dateString: String): Int { + return when { + dateString.contains("일 뒤") -> { + dateString.replace("일 뒤", "").trim().toIntOrNull() ?: 0 + } + else -> 0 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/util/RoomUtils.kt b/app/src/main/java/com/texthip/thip/util/RoomUtils.kt new file mode 100644 index 00000000..16fd6f09 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/util/RoomUtils.kt @@ -0,0 +1,18 @@ +package com.texthip.thip.util + + +object RoomUtils { + fun isRecruitingByType(type: String): Boolean { + return when (type) { + "recruiting" -> true + "playingAndRecruiting" -> false + "playing" -> false + "expired" -> false + else -> false + } + } + + fun getEndDateInDays(endDate: String): Int { + return DateUtils.extractDaysFromDeadline(endDate) + } +} \ No newline at end of file From 888d7d8544931e7b7d70ad96f3bcb2fe1a29abdc Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sat, 9 Aug 2025 18:02:03 +0900 Subject: [PATCH 66/68] =?UTF-8?q?[refactor]:=20genreManager=EC=97=90?= =?UTF-8?q?=EC=84=9C=20enum=20class=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/manager/Genre.kt | 27 ++++++++++++++++ .../texthip/thip/data/manager/GenreManager.kt | 32 ++++++------------- .../thip/data/repository/GroupRepository.kt | 9 +++--- .../ui/group/done/screen/GroupDoneScreen.kt | 3 +- .../makeroom/screen/GroupMakeRoomScreen.kt | 3 +- .../viewmodel/GroupMakeRoomUiState.kt | 3 +- .../viewmodel/GroupMakeRoomViewModel.kt | 8 ++--- .../component/GroupDeadlineRoomSection.kt | 16 ++++------ .../ui/group/myroom/screen/GroupMyScreen.kt | 2 +- .../room/screen/GroupRoomRecruitScreen.kt | 2 +- .../thip/ui/group/viewmodel/GroupViewModel.kt | 6 ++-- .../thip/{util => utils/rooms}/DateUtils.kt | 2 +- .../thip/utils/rooms/GenreExtensions.kt | 26 +++++++++++++++ .../thip/{util => utils/rooms}/RoomUtils.kt | 2 +- 14 files changed, 89 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/data/manager/Genre.kt rename app/src/main/java/com/texthip/thip/{util => utils/rooms}/DateUtils.kt (88%) create mode 100644 app/src/main/java/com/texthip/thip/utils/rooms/GenreExtensions.kt rename app/src/main/java/com/texthip/thip/{util => utils/rooms}/RoomUtils.kt (91%) diff --git a/app/src/main/java/com/texthip/thip/data/manager/Genre.kt b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt new file mode 100644 index 00000000..ab51e7de --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt @@ -0,0 +1,27 @@ +package com.texthip.thip.data.manager + +/** + * 도서 장르를 나타내는 enum class + */ +enum class Genre( + val displayKey: String, + val apiCategory: String +) { + LITERATURE("literature", "문학"), + SCIENCE_IT("science_it", "과학·IT"), + SOCIAL_SCIENCE("social_science", "사회과학"), + HUMANITIES("humanities", "인문학"), + ART("art", "예술"); + + companion object { + fun getDefault() = LITERATURE + + fun fromDisplayKey(displayKey: String): Genre? { + return entries.find { it.displayKey == displayKey } + } + + fun fromApiCategory(apiCategory: String): Genre? { + return entries.find { it.apiCategory == apiCategory } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt index 67514a81..912acb1f 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt @@ -1,36 +1,24 @@ package com.texthip.thip.data.manager -import android.content.Context -import com.texthip.thip.R -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton -class GenreManager @Inject constructor( - @param:ApplicationContext private val context: Context -) { +class GenreManager @Inject constructor() { - private val genres = listOf( - context.getString(R.string.literature), - context.getString(R.string.science_it), - context.getString(R.string.social_science), - context.getString(R.string.humanities), - context.getString(R.string.art) - ) + fun getGenres(): List { + return Genre.entries + } - fun getGenres(): List { - return genres + fun mapGenreToApiCategory(genre: Genre): String { + return genre.apiCategory } - fun mapGenreToApiCategory(genre: String): String { - return when (genre) { - context.getString(R.string.science_it) -> context.getString(R.string.api_genre_science_it) - else -> genre - } + fun getDefaultGenre(): Genre { + return Genre.getDefault() } - fun getDefaultGenre(): String { - return context.getString(R.string.literature) + fun getGenreByDisplayKey(displayKey: String): Genre? { + return Genre.fromDisplayKey(displayKey) } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt index dbbcc89f..a5c04326 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt @@ -2,6 +2,7 @@ package com.texthip.thip.data.repository import com.texthip.thip.data.manager.GenreManager import com.texthip.thip.data.manager.UserDataManager +import com.texthip.thip.data.manager.Genre import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.group.request.CreateRoomRequest import com.texthip.thip.data.model.group.request.RoomJoinRequest @@ -21,7 +22,7 @@ class GroupRepository @Inject constructor( ) { /** 장르 목록 조회 */ - fun getGenres(): Result> { + fun getGenres(): Result> { return Result.success(genreManager.getGenres()) } @@ -44,9 +45,9 @@ class GroupRepository @Inject constructor( } /** 카테고리별 모임방 섹션 조회 (마감임박/인기) */ - suspend fun getRoomSections(category: String = ""): Result = runCatching { - val finalCategory = category.ifEmpty { genreManager.getDefaultGenre() } - val apiCategory = genreManager.mapGenreToApiCategory(finalCategory) + suspend fun getRoomSections(genre: Genre? = null): Result = runCatching { + val selectedGenre = genre ?: genreManager.getDefaultGenre() + val apiCategory = genreManager.mapGenreToApiCategory(selectedGenre) groupService.getRooms(apiCategory) .handleBaseResponse() diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt index 94e7121d..17d3a24b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt @@ -24,14 +24,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R -import com.texthip.thip.data.model.group.response.MyRoomResponse import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.group.done.viewmodel.GroupDoneViewModel 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.util.RoomUtils +import com.texthip.thip.utils.rooms.RoomUtils @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt index 6c49dc8f..b3ee650a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt @@ -41,6 +41,7 @@ import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel 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.rooms.toDisplayStrings @Composable @@ -111,7 +112,7 @@ fun GroupMakeRoomScreen( Spacer(modifier = Modifier.padding(top = 12.dp)) GenreChipRow( modifier = Modifier.width(18.dp), - genres = uiState.genres, + genres = uiState.genres.toDisplayStrings(), selectedIndex = uiState.selectedGenreIndex, onSelect = viewModel::selectGenre ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt index 7cdf3601..9e95010a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt @@ -1,5 +1,6 @@ package com.texthip.thip.ui.group.makeroom.viewmodel +import com.texthip.thip.data.manager.Genre import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomRequest import java.time.LocalDate @@ -21,7 +22,7 @@ data class GroupMakeRoomUiState( val savedBooks: List = emptyList(), val groupBooks: List = emptyList(), val isLoadingBooks: Boolean = false, - val genres: List = emptyList() + val genres: List = emptyList() ) { // 유효성 검사 로직 val isDurationValid: Boolean diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index 045e8450..c40320b4 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.texthip.thip.R import com.texthip.thip.data.model.book.response.BookSavedResponse import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.manager.Genre import com.texthip.thip.data.repository.BookRepository import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.makeroom.mock.BookData @@ -181,12 +182,9 @@ class GroupMakeRoomViewModel @Inject constructor( val currentGenres = uiState.value.genres if (genreIndex >= 0 && genreIndex < currentGenres.size) { val genre = currentGenres[genreIndex] - return when (genre) { - "과학·IT" -> "과학/IT" - else -> genre - } + return genre.apiCategory } - return "문학" + return Genre.getDefault().apiCategory } fun clearError() { diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt index eb293739..20b791f2 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt @@ -30,11 +30,13 @@ import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.GenreChipRow import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.data.model.group.response.RoomMainList +import com.texthip.thip.data.manager.Genre import com.texthip.thip.data.model.group.response.RoomMainResponse 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.util.DateUtils +import com.texthip.thip.utils.rooms.DateUtils +import com.texthip.thip.utils.rooms.toDisplayStrings @SuppressLint("UnusedBoxWithConstraintsScope") @Composable @@ -64,14 +66,8 @@ fun GroupRoomDeadlineSection( val pageSpacing = (-(cardWidth - (cardWidth * scale)) / 2) + desiredGap - // 기본 장르 목록 - val defaultGenres = listOf( - stringResource(R.string.literature), - stringResource(R.string.science_it), - stringResource(R.string.social_science), - stringResource(R.string.humanities), - stringResource(R.string.art) - ) + // Genre enum을 현지화된 문자열로 변환 + val genreStrings = Genre.entries.toDisplayStrings() // 마감 임박 방 목록과 인기 방 목록을 섹션으로 구성 val roomSections = listOf( @@ -125,7 +121,7 @@ fun GroupRoomDeadlineSection( Spacer(Modifier.height(40.dp)) GenreChipRow( - genres = defaultGenres, + genres = genreStrings, selectedIndex = selectedGenreIndex, onSelect = onGenreSelect ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index 36787848..444e177d 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -34,7 +34,7 @@ import com.texthip.thip.ui.group.myroom.mock.RoomType import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography -import com.texthip.thip.util.RoomUtils +import com.texthip.thip.utils.rooms.RoomUtils @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index 4195baf0..a7cb1ecd 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -50,7 +50,7 @@ import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitViewModel 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.util.DateUtils +import com.texthip.thip.utils.rooms.DateUtils import kotlinx.coroutines.delay @Composable diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 05d3d3b4..663e322b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -3,7 +3,7 @@ package com.texthip.thip.ui.group.viewmodel import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.texthip.thip.R +import com.texthip.thip.data.manager.Genre import com.texthip.thip.data.repository.GroupRepository import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -126,10 +126,10 @@ class GroupViewModel @Inject constructor( if (selectedIndex >= 0 && selectedIndex < genres.size) { genres[selectedIndex] } else { - genres.firstOrNull() ?: context.getString(R.string.literature) + genres.firstOrNull() ?: Genre.getDefault() } } else { - context.getString(R.string.literature) + Genre.getDefault() } repository.getRoomSections(selectedGenre) diff --git a/app/src/main/java/com/texthip/thip/util/DateUtils.kt b/app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt similarity index 88% rename from app/src/main/java/com/texthip/thip/util/DateUtils.kt rename to app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt index 5f58e639..f5579e11 100644 --- a/app/src/main/java/com/texthip/thip/util/DateUtils.kt +++ b/app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.util +package com.texthip.thip.utils.rooms object DateUtils { diff --git a/app/src/main/java/com/texthip/thip/utils/rooms/GenreExtensions.kt b/app/src/main/java/com/texthip/thip/utils/rooms/GenreExtensions.kt new file mode 100644 index 00000000..bd6f2ccb --- /dev/null +++ b/app/src/main/java/com/texthip/thip/utils/rooms/GenreExtensions.kt @@ -0,0 +1,26 @@ +package com.texthip.thip.utils.rooms + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.texthip.thip.R +import com.texthip.thip.data.manager.Genre + +/** + * Genre enum을 UI에서 사용하기 위한 확장 함수들 + */ + +@Composable +fun Genre.toDisplayString(): String { + return when (this) { + Genre.LITERATURE -> stringResource(R.string.literature) + Genre.SCIENCE_IT -> stringResource(R.string.science_it) + Genre.SOCIAL_SCIENCE -> stringResource(R.string.social_science) + Genre.HUMANITIES -> stringResource(R.string.humanities) + Genre.ART -> stringResource(R.string.art) + } +} + +@Composable +fun List.toDisplayStrings(): List { + return this.map { it.toDisplayString() } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/util/RoomUtils.kt b/app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt similarity index 91% rename from app/src/main/java/com/texthip/thip/util/RoomUtils.kt rename to app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt index 16fd6f09..e206136f 100644 --- a/app/src/main/java/com/texthip/thip/util/RoomUtils.kt +++ b/app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.util +package com.texthip.thip.utils.rooms object RoomUtils { From 5a5a3400da71cc47e81453122e13dae438a66c59 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sat, 9 Aug 2025 19:56:41 +0900 Subject: [PATCH 67/68] =?UTF-8?q?[refactor]:=20screen=EC=9D=84=20content?= =?UTF-8?q?=EC=99=80=20=EB=B6=84=EB=A6=AC=20=EC=99=84=EB=A3=8C=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/manager/Genre.kt | 4 - .../ui/group/done/screen/GroupDoneScreen.kt | 79 ++++++++- .../makeroom/screen/GroupMakeRoomScreen.kt | 146 ++++++++++++---- .../ui/group/myroom/screen/GroupMyScreen.kt | 100 ++++++++++- .../room/screen/GroupRoomRecruitScreen.kt | 157 +++++++++++------- .../thip/ui/group/screen/GroupScreen.kt | 139 ++++++++++++++-- 6 files changed, 507 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/manager/Genre.kt b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt index ab51e7de..415a8d03 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/Genre.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt @@ -19,9 +19,5 @@ enum class Genre( fun fromDisplayKey(displayKey: String): Genre? { return entries.find { it.displayKey == displayKey } } - - fun fromApiCategory(apiCategory: String): Genre? { - return entries.find { it.apiCategory == apiCategory } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt index 17d3a24b..32f8d9ca 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt @@ -24,8 +24,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.MyRoomResponse import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.group.done.viewmodel.GroupDoneUiState import com.texthip.thip.ui.group.done.viewmodel.GroupDoneViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -39,6 +41,23 @@ fun GroupDoneScreen( viewModel: GroupDoneViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + + GroupDoneContent( + uiState = uiState, + onNavigateBack = onNavigateBack, + onRefresh = { viewModel.refreshData() }, + onLoadMore = { viewModel.loadMoreExpiredRooms() } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupDoneContent( + uiState: GroupDoneUiState, + onNavigateBack: () -> Unit = {}, + onRefresh: () -> Unit = {}, + onLoadMore: () -> Unit = {} +) { val listState = rememberLazyListState() // 무한 스크롤을 위한 로직 @@ -52,7 +71,7 @@ fun GroupDoneScreen( LaunchedEffect(shouldLoadMore) { if (shouldLoadMore && uiState.canLoadMore) { - viewModel.loadMoreExpiredRooms() + onLoadMore() } } @@ -66,7 +85,7 @@ fun GroupDoneScreen( PullToRefreshBox( isRefreshing = uiState.isLoading, - onRefresh = { viewModel.refreshData() }, + onRefresh = onRefresh, modifier = Modifier.fillMaxSize() ) { Column( @@ -112,8 +131,60 @@ fun GroupDoneScreen( @Composable fun GroupDoneScreenPreview() { ThipTheme { - // Preview에서는 ViewModel을 사용할 수 없으므로 기본 레이아웃만 표시 - GroupDoneScreen() + GroupDoneContent( + uiState = GroupDoneUiState( + userName = "김독서", + expiredRooms = listOf( + MyRoomResponse( + roomId = 1, + roomName = "🌙 미드나이트 라이브러리 함께읽기", + bookImageUrl = "https://picsum.photos/300/400?1", + memberCount = 18, + recruitCount = 20, + endDate = "2025-01-31", + type = "EXPIRED" + ), + MyRoomResponse( + roomId = 2, + roomName = "📚 현대문학 깊이읽기 모임", + bookImageUrl = "https://picsum.photos/300/400?2", + memberCount = 12, + recruitCount = 15, + endDate = "2024-12-28", + type = "EXPIRED" + ), + MyRoomResponse( + roomId = 3, + roomName = "🔬 과학책으로 세상보기", + bookImageUrl = "https://picsum.photos/300/400?3", + memberCount = 25, + recruitCount = 30, + endDate = "2024-12-15", + type = "EXPIRED" + ), + MyRoomResponse( + roomId = 4, + roomName = "✨ 철학 고전 탐구하기", + bookImageUrl = "https://picsum.photos/300/400?4", + memberCount = 10, + recruitCount = 12, + endDate = "2024-11-20", + type = "EXPIRED" + ), + MyRoomResponse( + roomId = 5, + roomName = "🎨 예술과 문학의 만남", + bookImageUrl = "https://picsum.photos/300/400?5", + memberCount = 16, + recruitCount = 20, + endDate = "2024-10-31", + type = "EXPIRED" + ) + ), + isLoading = false, + hasMore = true + ) + ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt index b3ee650a..a4fc96ff 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt @@ -24,8 +24,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.manager.Genre import com.texthip.thip.ui.common.buttons.GenreChipRow import com.texthip.thip.ui.common.buttons.ToggleSwitchButton import com.texthip.thip.ui.common.forms.WarningTextField @@ -37,6 +38,7 @@ import com.texthip.thip.ui.group.makeroom.component.GroupSelectBook import com.texthip.thip.ui.group.makeroom.component.GroupMemberLimitPicker import com.texthip.thip.ui.group.makeroom.component.SectionDivider import com.texthip.thip.ui.group.makeroom.mock.BookData +import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomUiState import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -46,13 +48,12 @@ import com.texthip.thip.utils.rooms.toDisplayStrings @Composable fun GroupMakeRoomScreen( - viewModel: GroupMakeRoomViewModel, onNavigateBack: () -> Unit, onGroupCreated: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + viewModel: GroupMakeRoomViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() - val scrollState = rememberScrollState() // 에러 메시지 표시 LaunchedEffect(uiState.errorMessage) { @@ -61,6 +62,54 @@ fun GroupMakeRoomScreen( } } + GroupMakeRoomContent( + uiState = uiState, + onNavigateBack = onNavigateBack, + onGroupCreated = onGroupCreated, + onCreateGroup = { + viewModel.createGroup( + onSuccess = { roomId -> + // TODO: 생성된 roomId를 사용하여 해당 방으로 이동할 수 있음 + onGroupCreated() + }, + onError = { errorMessage -> + // TODO: 에러 메시지 표시 (토스트 메시지 등) + // 현재는 uiState.errorMessage를 통해 처리 + } + ) + }, + onSelectBook = viewModel::selectBook, + onToggleBookSearchSheet = viewModel::toggleBookSearchSheet, + onSelectGenre = viewModel::selectGenre, + onUpdateRoomTitle = viewModel::updateRoomTitle, + onUpdateRoomDescription = viewModel::updateRoomDescription, + onSetDateRange = viewModel::setDateRange, + onSetMemberLimit = viewModel::setMemberLimit, + onTogglePrivate = viewModel::togglePrivate, + onUpdatePassword = viewModel::updatePassword, + modifier = modifier + ) +} + +@Composable +fun GroupMakeRoomContent( + modifier: Modifier = Modifier, + uiState: GroupMakeRoomUiState, + onNavigateBack: () -> Unit = {}, + onGroupCreated: () -> Unit = {}, // 그룹이 만들어졌을때 로직 + onCreateGroup: () -> Unit = {}, + onSelectBook: (BookData) -> Unit = {}, + onToggleBookSearchSheet: (Boolean) -> Unit = {}, + onSelectGenre: (Int) -> Unit = {}, + onUpdateRoomTitle: (String) -> Unit = {}, + onUpdateRoomDescription: (String) -> Unit = {}, + onSetDateRange: (java.time.LocalDate, java.time.LocalDate) -> Unit = { _, _ -> }, + onSetMemberLimit: (Int) -> Unit = {}, + onTogglePrivate: (Boolean) -> Unit = {}, + onUpdatePassword: (String) -> Unit = {} +) { + val scrollState = rememberScrollState() + Box { Column( modifier = modifier @@ -73,18 +122,7 @@ fun GroupMakeRoomScreen( title = stringResource(R.string.group_making_group), isRightButtonEnabled = uiState.isFormValid && !uiState.isLoading, onLeftClick = onNavigateBack, - onRightClick = { - viewModel.createGroup( - onSuccess = { roomId -> - // TODO: 생성된 roomId를 사용하여 해당 방으로 이동할 수 있음 - onGroupCreated() - }, - onError = { errorMessage -> - // TODO: 에러 메시지 표시 (토스트 메시지 등) - // 현재는 uiState.errorMessage를 통해 처리 - } - ) - } + onRightClick = onCreateGroup ) Column( @@ -98,8 +136,8 @@ fun GroupMakeRoomScreen( GroupSelectBook( selectedBook = uiState.selectedBook, - onChangeBookClick = { viewModel.toggleBookSearchSheet(true) }, - onSelectBookClick = { viewModel.toggleBookSearchSheet(true) } + onChangeBookClick = { onToggleBookSearchSheet(true) }, + onSelectBookClick = { onToggleBookSearchSheet(true) } ) SectionDivider() @@ -114,7 +152,7 @@ fun GroupMakeRoomScreen( modifier = Modifier.width(18.dp), genres = uiState.genres.toDisplayStrings(), selectedIndex = uiState.selectedGenreIndex, - onSelect = viewModel::selectGenre + onSelect = onSelectGenre ) Spacer(modifier = Modifier.height(12.dp)) @@ -136,7 +174,7 @@ fun GroupMakeRoomScreen( hint = stringResource(R.string.group_room_title_hint), value = uiState.roomTitle, maxLength = 15, - onValueChange = viewModel::updateRoomTitle + onValueChange = onUpdateRoomTitle ) SectionDivider() @@ -145,20 +183,20 @@ fun GroupMakeRoomScreen( title = stringResource(R.string.group_room_explain), hint = stringResource(R.string.group_room_explain_hint), value = uiState.roomDescription, - onValueChange = viewModel::updateRoomDescription + onValueChange = onUpdateRoomDescription ) SectionDivider() GroupRoomDurationPicker( - onDateRangeSelected = viewModel::setDateRange + onDateRangeSelected = onSetDateRange ) SectionDivider() GroupMemberLimitPicker( selectedCount = uiState.memberLimit, - onCountSelected = viewModel::setMemberLimit + onCountSelected = onSetMemberLimit ) SectionDivider() @@ -181,7 +219,7 @@ fun GroupMakeRoomScreen( ) ToggleSwitchButton( isChecked = uiState.isPrivate, - onToggleChange = viewModel::togglePrivate + onToggleChange = onTogglePrivate ) } @@ -189,7 +227,7 @@ fun GroupMakeRoomScreen( Spacer(modifier = Modifier.height(12.dp)) WarningTextField( value = uiState.password, - onValueChange = viewModel::updatePassword, + onValueChange = onUpdatePassword, hint = stringResource(R.string.group_password_hint), showWarning = uiState.password.isNotEmpty() && uiState.password.length < 4, warningMessage = stringResource(R.string.group_private_warning_message), @@ -208,13 +246,13 @@ fun GroupMakeRoomScreen( if (uiState.showBookSearchSheet) { GroupBookSearchBottomSheet( - onDismiss = { viewModel.toggleBookSearchSheet(false) }, + onDismiss = { onToggleBookSearchSheet(false) }, onBookSelect = { book: BookData -> - viewModel.selectBook(book) - viewModel.toggleBookSearchSheet(false) + onSelectBook(book) + onToggleBookSearchSheet(false) }, onRequestBook = { - viewModel.toggleBookSearchSheet(false) + onToggleBookSearchSheet(false) }, savedBooks = uiState.savedBooks, groupBooks = uiState.groupBooks, @@ -240,13 +278,51 @@ fun GroupMakeRoomScreen( @Preview @Composable private fun GroupMakeRoomScreenPreview() { - val mockViewModel: GroupMakeRoomViewModel = viewModel() - ThipTheme { - GroupMakeRoomScreen( - viewModel = mockViewModel, - onNavigateBack = { }, - onGroupCreated = { } + GroupMakeRoomContent( + uiState = GroupMakeRoomUiState( + selectedBook = BookData( + title = "미드나이트 라이브러리", + imageUrl = "https://picsum.photos/300/400?1", + author = "매트 헤이그", + isbn = "9788937477263" + ), + selectedGenreIndex = 2, + roomTitle = "인생에 대해 고민하는 독서모임", + roomDescription = "매트 헤이그의 미드나이트 라이브러리를 함께 읽으며 인생의 가능성과 선택에 대해 이야기해요. 따뜻한 마음으로 서로의 이야기를 들어주실 분들과 함께하고 싶어요.", + memberLimit = 12, + isPrivate = true, + password = "1234", + genres = Genre.entries.toList(), + savedBooks = listOf( + BookData( + title = "코스모스", + imageUrl = "https://picsum.photos/300/400?2", + author = "칼 세이건", + isbn = "9788983711892" + ), + BookData( + title = "사피엔스", + imageUrl = "https://picsum.photos/300/400?3", + author = "유발 하라리", + isbn = "9788934972464" + ) + ), + groupBooks = listOf( + BookData( + title = "1984", + imageUrl = "https://picsum.photos/300/400?4", + author = "조지 오웰", + isbn = "9788937460777" + ), + BookData( + title = "어린왕자", + imageUrl = "https://picsum.photos/300/400?5", + author = "생텍쥐페리", + isbn = "9788932917245" + ) + ) + ) ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index 444e177d..011b68f9 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -23,6 +23,7 @@ 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 com.texthip.thip.R @@ -31,7 +32,9 @@ import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.group.myroom.component.GroupMyRoomFilterRow import com.texthip.thip.ui.group.myroom.mock.RoomType +import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyUiState import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel +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.rooms.RoomUtils @@ -44,6 +47,27 @@ fun GroupMyScreen( viewModel: GroupMyViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + + GroupMyContent( + uiState = uiState, + onCardClick = onCardClick, + onNavigateBack = onNavigateBack, + onRefresh = { viewModel.refreshData() }, + onLoadMore = { viewModel.loadMoreMyRooms() }, + onChangeRoomType = { viewModel.changeRoomType(it) } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupMyContent( + uiState: GroupMyUiState, + onCardClick: (MyRoomResponse) -> Unit = {}, + onNavigateBack: () -> Unit = {}, + onRefresh: () -> Unit = {}, + onLoadMore: () -> Unit = {}, + onChangeRoomType: (RoomType) -> Unit = {} +) { val listState = rememberLazyListState() // 무한 스크롤 로직 @@ -57,7 +81,7 @@ fun GroupMyScreen( LaunchedEffect(shouldLoadMore) { if (shouldLoadMore && uiState.canLoadMore) { - viewModel.loadMoreMyRooms() + onLoadMore() } } @@ -81,7 +105,7 @@ fun GroupMyScreen( PullToRefreshBox( isRefreshing = uiState.isLoading, - onRefresh = { viewModel.refreshData() }, + onRefresh = onRefresh, modifier = Modifier.fillMaxSize() ) { Column( @@ -116,7 +140,7 @@ fun GroupMyScreen( } else -> RoomType.PLAYING_AND_RECRUITING } - viewModel.changeRoomType(newRoomType) + onChangeRoomType(newRoomType) } ) @@ -167,4 +191,72 @@ fun GroupMyScreen( } } - +@Preview +@Composable +fun GroupMyScreenPreview() { + ThipTheme { + GroupMyContent( + uiState = GroupMyUiState( + myRooms = listOf( + MyRoomResponse( + roomId = 1, + roomName = "🌙 미드나이트 라이브러리 함께읽기", + bookImageUrl = "https://picsum.photos/300/400?1", + memberCount = 18, + recruitCount = 20, + type = "RECRUITING", + endDate = "2025-02-15" + ), + MyRoomResponse( + roomId = 2, + roomName = "📚 현대문학 깊이 탐구하기", + bookImageUrl = "https://picsum.photos/300/400?2", + memberCount = 12, + recruitCount = 15, + type = "PLAYING", + endDate = "2025-01-28" + ), + MyRoomResponse( + roomId = 3, + roomName = "🔬 과학책으로 세상 이해하기", + bookImageUrl = "https://picsum.photos/300/400?3", + memberCount = 25, + recruitCount = 30, + type = "RECRUITING", + endDate = "2025-03-01" + ), + MyRoomResponse( + roomId = 4, + roomName = "✨ 철학 고전 함께 읽기", + bookImageUrl = "https://picsum.photos/300/400?4", + memberCount = 8, + recruitCount = 12, + type = "PLAYING", + endDate = "2025-02-10" + ), + MyRoomResponse( + roomId = 5, + roomName = "🎨 예술과 문학의 아름다운 만남", + bookImageUrl = "https://picsum.photos/300/400?5", + memberCount = 6, + recruitCount = 10, + type = "RECRUITING", + endDate = "2025-02-20" + ), + MyRoomResponse( + roomId = 6, + roomName = "💭 심리학 도서 탐험대", + bookImageUrl = "https://picsum.photos/300/400?6", + memberCount = 14, + recruitCount = 18, + type = "PLAYING", + endDate = "2025-01-30" + ) + ), + currentRoomType = RoomType.PLAYING_AND_RECRUITING, + isLoading = false, + hasMore = true + ) + ) + } +} diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index a7cb1ecd..c06e061c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -42,10 +42,8 @@ import com.texthip.thip.ui.common.cards.CardRoomBook import com.texthip.thip.ui.common.modal.DialogPopup import com.texthip.thip.ui.common.modal.ToastWithDate import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar -import com.texthip.thip.ui.group.myroom.mock.GroupBookData import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitUiState import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -56,13 +54,11 @@ import kotlinx.coroutines.delay @Composable fun GroupRoomRecruitScreen( roomId: Int, - viewModel: GroupRoomRecruitViewModel = hiltViewModel(), onRecommendationClick: (RecommendRoomResponse) -> Unit = {}, onNavigateToGroupScreen: (String) -> Unit = {}, // GroupScreen으로 네비게이션 + 토스트 메시지 - onBackClick: () -> Unit = {} // 뒤로가기 + onBackClick: () -> Unit = {}, // 뒤로가기 + viewModel: GroupRoomRecruitViewModel = hiltViewModel() ) { - val context = LocalContext.current - val uiState by viewModel.uiState.collectAsState() // 데이터 로딩 @@ -77,6 +73,33 @@ fun GroupRoomRecruitScreen( viewModel.onNavigatedToGroupScreen() } } + + GroupRoomRecruitContent( + uiState = uiState, + onRecommendationClick = onRecommendationClick, + onBackClick = onBackClick, + onParticipationClick = { viewModel.onParticipationClick() }, + onCancelParticipationClick = { title, description -> viewModel.onCancelParticipationClick(title, description) }, + onCloseRecruitmentClick = { title, description -> viewModel.onCloseRecruitmentClick(title, description) }, + onDialogConfirm = { viewModel.onDialogConfirm() }, + onDialogCancel = { viewModel.onDialogCancel() }, + onHideToast = { viewModel.hideToast() } + ) +} + +@Composable +fun GroupRoomRecruitContent( + uiState: GroupRoomRecruitUiState, + onRecommendationClick: (RecommendRoomResponse) -> Unit = {}, + onBackClick: () -> Unit = {}, + onParticipationClick: () -> Unit = {}, + onCancelParticipationClick: (String, String) -> Unit = { _, _ -> }, + onCloseRecruitmentClick: (String, String) -> Unit = { _, _ -> }, + onDialogConfirm: () -> Unit = {}, + onDialogCancel: () -> Unit = {}, + onHideToast: () -> Unit = {} +) { + val context = LocalContext.current Box(Modifier.fillMaxSize()) { // 로딩 상태 @@ -366,20 +389,20 @@ fun GroupRoomRecruitScreen( onClick = { when (buttonType) { GroupBottomButtonType.JOIN -> { - viewModel.onParticipationClick() + onParticipationClick() } GroupBottomButtonType.CANCEL -> { - viewModel.onCancelParticipationClick( - dialogTitle = context.getString(R.string.group_participant_cancel_popup), - dialogDescription = context.getString(R.string.group_participant_cancel_comment) + onCancelParticipationClick( + context.getString(R.string.group_participant_cancel_popup), + context.getString(R.string.group_participant_cancel_comment) ) } GroupBottomButtonType.CLOSE -> { - viewModel.onCloseRecruitmentClick( - dialogTitle = context.getString(R.string.group_participant_close_popup), - dialogDescription = context.getString(R.string.group_participant_close_comment) + onCloseRecruitmentClick( + context.getString(R.string.group_participant_close_popup), + context.getString(R.string.group_participant_close_comment) ) } } @@ -423,12 +446,8 @@ fun GroupRoomRecruitScreen( DialogPopup( title = uiState.dialogTitle, description = uiState.dialogDescription, - onConfirm = { - viewModel.onDialogConfirm() - }, - onCancel = { - viewModel.onDialogCancel() - } + onConfirm = onDialogConfirm, + onCancel = onDialogCancel ) } } @@ -438,7 +457,7 @@ fun GroupRoomRecruitScreen( LaunchedEffect(uiState.showToast, uiState.shouldNavigateToGroupScreen) { if (uiState.showToast && !uiState.shouldNavigateToGroupScreen) { delay(3000) - viewModel.hideToast() + onHideToast() } } } @@ -447,46 +466,64 @@ fun GroupRoomRecruitScreen( @Composable fun GroupRoomRecruitScreenPreview() { ThipTheme { - val recommendations = listOf( - GroupCardItemRoomData( - id = 1, - title = "일본 소설 좋아하는 사람들", - participants = 19, - maxParticipants = 25, - isRecruiting = true, - endDate = 2, + GroupRoomRecruitContent( + uiState = GroupRoomRecruitUiState( + isLoading = false, + roomDetail = com.texthip.thip.data.model.group.response.RoomRecruitingResponse( + isHost = false, + isJoining = false, + roomId = 1, + roomName = "🌙 미드나이트 라이브러리 함께읽기", + roomImageUrl = "https://picsum.photos/400/600?1", + isPublic = false, + progressStartDate = "2025.02.01", + progressEndDate = "2025.02.28", + recruitEndDate = "D-5", + category = "문학", + roomDescription = "매트 헤이그의 미드나이트 라이브러리를 함께 읽으며 인생의 가능성과 선택에 대해 이야기해요. 각자의 삶에서 후회했던 순간들을 공유하고, 서로 위로하며 성장하는 시간을 가져보아요. 따뜻한 마음으로 서로의 이야기를 들어주실 분들과 함께하고 싶습니다.", + memberCount = 18, + recruitCount = 20, + isbn = "9788937477263", + bookImageUrl = "https://picsum.photos/300/400?book1", + bookTitle = "미드나이트 라이브러리", + authorName = "매트 헤이그", + bookDescription = "삶과 죽음 사이, 후회와 가능성 사이에서 펼쳐지는 놀라운 이야기. 인생의 무한한 가능성을 탐험하는 감동적인 소설", + publisher = "인플루엔셜", + recommendRooms = listOf( + RecommendRoomResponse( + roomId = 2, + roomImageUrl = "https://picsum.photos/300/400?rec1", + roomName = "📚 현대문학 깊이 탐구하기", + memberCount = 12, + recruitCount = 15, + recruitEndDate = "D-3" + ), + RecommendRoomResponse( + roomId = 3, + roomImageUrl = "https://picsum.photos/300/400?rec2", + roomName = "✨ 철학 소설로 삶을 되돌아보기", + memberCount = 8, + recruitCount = 12, + recruitEndDate = "D-7" + ), + RecommendRoomResponse( + roomId = 4, + roomImageUrl = "https://picsum.photos/300/400?rec3", + roomName = "🎭 인간 심리를 다룬 소설 읽기", + memberCount = 15, + recruitCount = 18, + recruitEndDate = "D-2" + ) + ) + ), + currentButtonType = GroupBottomButtonType.JOIN, + showDialog = false, + showToast = false, + toastMessage = "", + dialogTitle = "", + dialogDescription = "", + shouldNavigateToGroupScreen = false ) ) - - val bookData = GroupBookData( - title = "심장보다 단단한 토마토 한 알", - author = "고선지", - publisher = "푸른출판사", - description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집.", - imageUrl = null - ) - - val detailJoin = GroupRoomData( - id = 1, - title = "시집만 읽는 사람들 3월", - isSecret = true, - description = "'시집만 읽는 사람들' 3월 모임입니다.", - startDate = "2025.01.12", - endDate = "2025.02.12", - members = 22, - maxMembers = 30, - daysLeft = 4, - genre = "문학", - bookData = bookData, - recommendations = recommendations, - buttonType = GroupBottomButtonType.JOIN - ) - - GroupRoomRecruitScreen( - roomId = 1, - onRecommendationClick = {}, - onNavigateToGroupScreen = {}, - onBackClick = {} - ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index 9bbfe855..c49d0723 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.RoomMainList import com.texthip.thip.ui.common.buttons.FloatingButton import com.texthip.thip.ui.common.modal.ToastWithDate import com.texthip.thip.ui.common.topappbar.LogoTopAppBar @@ -31,6 +32,7 @@ import com.texthip.thip.ui.group.myroom.component.GroupMySectionHeader import com.texthip.thip.ui.group.myroom.component.GroupPager import com.texthip.thip.ui.group.myroom.component.GroupRoomDeadlineSection import com.texthip.thip.ui.group.myroom.component.GroupSearchTextField +import com.texthip.thip.ui.group.viewmodel.GroupUiState import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -53,6 +55,39 @@ fun GroupScreen( viewModel.refreshDataOnScreenEnter() } val uiState by viewModel.uiState.collectAsState() + + GroupContent( + uiState = uiState, + onNavigateToMakeRoom = onNavigateToMakeRoom, + onNavigateToGroupDone = onNavigateToGroupDone, + onNavigateToAlarm = onNavigateToAlarm, + onNavigateToGroupSearch = onNavigateToGroupSearch, + onNavigateToGroupMy = onNavigateToGroupMy, + onNavigateToGroupRecruit = onNavigateToGroupRecruit, + onNavigateToGroupRoom = onNavigateToGroupRoom, + onRefreshGroupData = { viewModel.refreshGroupData() }, + onCardVisible = { cardIndex -> viewModel.onCardVisible(cardIndex) }, + onSelectGenre = { genreIndex -> viewModel.selectGenre(genreIndex) }, + onHideToast = { viewModel.hideToast() } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupContent( + uiState: GroupUiState, + onNavigateToMakeRoom: () -> Unit = {}, + onNavigateToGroupDone: () -> Unit = {}, + onNavigateToAlarm: () -> Unit = {}, + onNavigateToGroupSearch: () -> Unit = {}, + onNavigateToGroupMy: () -> Unit = {}, + onNavigateToGroupRecruit: (Int) -> Unit = {}, + onNavigateToGroupRoom: (Int) -> Unit = {}, + onRefreshGroupData: () -> Unit = {}, + onCardVisible: (Int) -> Unit = {}, + onSelectGenre: (Int) -> Unit = {}, + onHideToast: () -> Unit = {} +) { val scrollState = rememberScrollState() Box( @@ -60,9 +95,7 @@ fun GroupScreen( ) { PullToRefreshBox( isRefreshing = uiState.isRefreshing, - onRefresh = { - viewModel.refreshGroupData() - }, + onRefresh = onRefreshGroupData, modifier = Modifier.fillMaxSize() ) { Column( @@ -96,9 +129,7 @@ fun GroupScreen( onCardClick = { joinedRoom -> onNavigateToGroupRoom(joinedRoom.roomId) }, - onCardVisible = { cardIndex -> - viewModel.onCardVisible(cardIndex) - } + onCardVisible = onCardVisible ) Spacer(Modifier.height(32.dp)) @@ -115,9 +146,7 @@ fun GroupScreen( roomMainList = uiState.roomMainList, selectedGenreIndex = uiState.selectedGenreIndex, errorMessage = uiState.roomSectionsError, - onGenreSelect = { genreIndex -> - viewModel.selectGenre(genreIndex) - }, + onGenreSelect = onSelectGenre, onRoomClick = { room -> onNavigateToGroupRecruit(room.roomId) } @@ -147,7 +176,7 @@ fun GroupScreen( LaunchedEffect(uiState.showToast) { if (uiState.showToast) { delay(3000L) - viewModel.hideToast() + onHideToast() } } } @@ -157,6 +186,94 @@ fun GroupScreen( @Composable fun PreviewGroupScreen() { ThipTheme { - GroupScreen() + GroupContent( + uiState = GroupUiState( + userName = "김독서", + myJoinedRooms = listOf( + com.texthip.thip.data.model.group.response.JoinedRoomResponse( + roomId = 1, + bookImageUrl = "https://picsum.photos/300/400?joined1", + bookTitle = "미드나이트 라이브러리", + memberCount = 18, + userPercentage = 75 + ), + com.texthip.thip.data.model.group.response.JoinedRoomResponse( + roomId = 2, + bookImageUrl = "https://picsum.photos/300/400?joined2", + bookTitle = "코스모스", + memberCount = 25, + userPercentage = 42 + ), + com.texthip.thip.data.model.group.response.JoinedRoomResponse( + roomId = 3, + bookImageUrl = "https://picsum.photos/300/400?joined3", + bookTitle = "사피엔스", + memberCount = 15, + userPercentage = 88 + ) + ), + roomMainList = RoomMainList( + deadlineRoomList = listOf( + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 4, + bookImageUrl = "https://picsum.photos/300/400?deadline1", + roomName = "🌙 미드나이트 라이브러리 함께읽기", + recruitCount = 20, + memberCount = 18, + deadlineDate = "D-2" + ), + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 5, + bookImageUrl = "https://picsum.photos/300/400?deadline2", + roomName = "📚 현대문학 깊이 탐구하기", + recruitCount = 15, + memberCount = 12, + deadlineDate = "D-3" + ), + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 6, + bookImageUrl = "https://picsum.photos/300/400?deadline3", + roomName = "🔬 과학책으로 세상 이해하기", + recruitCount = 30, + memberCount = 25, + deadlineDate = "D-5" + ) + ), + popularRoomList = listOf( + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 7, + bookImageUrl = "https://picsum.photos/300/400?popular1", + roomName = "✨ 철학 고전 함께 읽기", + recruitCount = 12, + memberCount = 10, + deadlineDate = "D-7" + ), + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 8, + bookImageUrl = "https://picsum.photos/300/400?popular2", + roomName = "🎨 예술과 문학의 만남", + recruitCount = 20, + memberCount = 16, + deadlineDate = "D-10" + ), + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 9, + bookImageUrl = "https://picsum.photos/300/400?popular3", + roomName = "💭 심리학 도서 탐험대", + recruitCount = 18, + memberCount = 14, + deadlineDate = "D-12" + ) + ) + ), + selectedGenreIndex = 2, + isRefreshing = false, + hasMoreMyGroups = true, + isLoadingMoreMyGroups = false, + roomSectionsError = null, + showToast = false, + toastMessage = "" + ) + ) } } \ No newline at end of file From e05e4253c5f349301952fe8877b24a805c39a313 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sat, 9 Aug 2025 22:20:53 +0900 Subject: [PATCH 68/68] =?UTF-8?q?[refactor]:=20=EC=9E=A5=EB=A5=B4=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/texthip/thip/data/manager/Genre.kt | 5 +++-- .../main/java/com/texthip/thip/data/manager/GenreManager.kt | 2 +- .../ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/manager/Genre.kt b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt index 415a8d03..3bc7a8dc 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/Genre.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt @@ -5,10 +5,11 @@ package com.texthip.thip.data.manager */ enum class Genre( val displayKey: String, - val apiCategory: String + val apiCategory: String, + val networkApiCategory: String = apiCategory ) { LITERATURE("literature", "문학"), - SCIENCE_IT("science_it", "과학·IT"), + SCIENCE_IT("science_it", "과학·IT", "과학/IT"), SOCIAL_SCIENCE("social_science", "사회과학"), HUMANITIES("humanities", "인문학"), ART("art", "예술"); diff --git a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt index 912acb1f..c4cefe27 100644 --- a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt +++ b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt @@ -11,7 +11,7 @@ class GenreManager @Inject constructor() { } fun mapGenreToApiCategory(genre: Genre): String { - return genre.apiCategory + return genre.networkApiCategory } fun getDefaultGenre(): Genre { diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index c40320b4..4652e06b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -182,9 +182,9 @@ class GroupMakeRoomViewModel @Inject constructor( val currentGenres = uiState.value.genres if (genreIndex >= 0 && genreIndex < currentGenres.size) { val genre = currentGenres[genreIndex] - return genre.apiCategory + return genre.networkApiCategory } - return Genre.getDefault().apiCategory + return Genre.getDefault().networkApiCategory } fun clearError() {