From 3088a513f147e615ab29ef3dc8e9029bba021243 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 19 Aug 2025 01:02:21 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20Response,=20Service,=20Repository=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rooms/response/RoomsSearchResponse.kt | 22 +++++++++++++++++++ .../thip/data/repository/RoomsRepository.kt | 14 ++++++++++++ .../texthip/thip/data/service/RoomsService.kt | 11 ++++++++++ 3 files changed, 47 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsSearchResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsSearchResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsSearchResponse.kt new file mode 100644 index 00000000..b869356b --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsSearchResponse.kt @@ -0,0 +1,22 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsSearchResponse( + @SerialName("roomList") val roomList: List = emptyList(), + @SerialName("nextCursor") val nextCursor: String? = null, + @SerialName("isLast") val isLast: Boolean = true +) + +@Serializable +data class SearchRoomItem( + @SerialName("roomId") val roomId: Int = 0, + @SerialName("bookImageUrl") val bookImageUrl: String? = null, + @SerialName("roomName") val roomName: String = "", + @SerialName("memberCount") val memberCount: Int = 0, + @SerialName("recruitCount") val recruitCount: Int = 0, + @SerialName("deadlineDate") val deadlineDate: String = "", + @SerialName("isPublic") val isPublic: Boolean = true +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt index 290394d9..4618c660 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt @@ -19,6 +19,7 @@ import com.texthip.thip.data.model.rooms.response.RoomCloseResponse import com.texthip.thip.data.model.rooms.response.RoomMainList import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse import com.texthip.thip.data.model.rooms.response.RoomSecretRoomResponse +import com.texthip.thip.data.model.rooms.response.RoomsSearchResponse import com.texthip.thip.data.service.RoomsService import javax.inject.Inject import javax.inject.Singleton @@ -121,6 +122,19 @@ class RoomsRepository @Inject constructor( response } + /** 모임방 검색 */ + suspend fun searchRooms( + keyword: String, + category: String, + sort: String = "deadline", + isFinalized: Boolean = false, + cursor: String? = null + ): Result = runCatching { + roomsService.searchRooms(keyword, category, sort, isFinalized, cursor) + .handleBaseResponse() + .getOrThrow() + } + /** 기록장 API들 */ suspend fun getRoomsPlaying( diff --git a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt index 049b5aea..b7365b58 100644 --- a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt @@ -28,6 +28,7 @@ import com.texthip.thip.data.model.rooms.response.RoomsPostsLikesResponse import com.texthip.thip.data.model.rooms.response.RoomsPostsResponse import com.texthip.thip.data.model.rooms.response.RoomsRecordResponse import com.texthip.thip.data.model.rooms.response.RoomsRecordsPinResponse +import com.texthip.thip.data.model.rooms.response.RoomsSearchResponse import com.texthip.thip.data.model.rooms.response.RoomsUsersResponse import com.texthip.thip.data.model.rooms.response.RoomsVoteResponse import retrofit2.http.Body @@ -88,6 +89,16 @@ interface RoomsService { @Path("roomId") roomId: Int ): BaseResponse + /** 모임방 검색 */ + @GET("rooms/search") + suspend fun searchRooms( + @Query("keyword") keyword: String, + @Query("category") category: String, + @Query("sort") sort: String = "deadline", + @Query("isFinalized") isFinalized: Boolean = false, + @Query("cursor") cursor: String? = null + ): BaseResponse + /** 기록장 API들 */ From 9015bf331415f6a5799177e69784cddd80a21426 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 19 Aug 2025 01:02:59 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20ViewModel=20=EA=B5=AC=ED=98=84=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/group/screen/GroupScreen.kt | 2 +- .../search/viewmodel/GroupSearchViewModel.kt | 302 ++++++++++++++++-- 2 files changed, 275 insertions(+), 29 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 dd097dbf..8ca4dac4 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 @@ -108,7 +108,7 @@ fun GroupContent( // 검색창 GroupSearchTextField( - modifier = Modifier.padding(top = 16.dp, bottom = 32.dp), + modifier = Modifier.padding(top = 72.dp, bottom = 32.dp), onClick = onNavigateToGroupSearch ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt index c5914b96..b8a20a01 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt @@ -2,9 +2,14 @@ package com.texthip.thip.ui.group.search.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.manager.Genre import com.texthip.thip.data.model.book.response.RecentSearchItem +import com.texthip.thip.data.model.rooms.response.SearchRoomItem import com.texthip.thip.data.repository.RecentSearchRepository +import com.texthip.thip.data.repository.RoomsRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -13,34 +18,268 @@ import kotlinx.coroutines.launch import javax.inject.Inject data class GroupSearchUiState( + val searchQuery: String = "", + + // 상태 관리 단순화 - boolean 필드 사용 + val isInitial: Boolean = true, + val isLiveSearching: Boolean = false, + val isCompleteSearching: Boolean = false, + + // 검색 결과 및 데이터 + val searchResults: List = emptyList(), val recentSearches: List = emptyList(), - val isLoading: Boolean = false, - val error: String? = null -) + val genres: List = emptyList(), + + // 필터링 상태 + val selectedGenre: Genre? = null, + val selectedSort: String = "deadline", // "deadline" 또는 "memberCount" + + // 로딩 상태 + val isSearching: Boolean = false, + val isLoadingMore: Boolean = false, + + // 페이징 정보 + val nextCursor: String? = null, + val hasMore: Boolean = true, + + // 에러/토스트 + val error: String? = null, + val showToast: Boolean = false, + val toastMessage: String = "" +) { + val hasResults: Boolean get() = searchResults.isNotEmpty() + val canLoadMore: Boolean get() = hasMore && !isSearching && !isLoadingMore + val showEmptyState: Boolean get() = searchQuery.isNotBlank() && searchResults.isEmpty() && !isSearching +} @HiltViewModel class GroupSearchViewModel @Inject constructor( + private val roomsRepository: RoomsRepository, private val recentSearchRepository: RecentSearchRepository ) : ViewModel() { private val _uiState = MutableStateFlow(GroupSearchUiState()) val uiState: StateFlow = _uiState.asStateFlow() + private var searchJob: Job? = null + private var loadMoreJob: Job? = null + // Map 기반 빠른 최근 검색어 관리 private val recentSearchMap = mutableMapOf() init { - loadRecentSearches() + loadInitialData() } private fun updateState(update: (GroupSearchUiState) -> GroupSearchUiState) { _uiState.update(update) } - fun loadRecentSearches() { + private fun loadInitialData() { + loadGenres() + loadRecentSearches() + } + + private fun loadGenres() { viewModelScope.launch { - updateState { it.copy(isLoading = true) } + roomsRepository.getGenres() + .onSuccess { genres -> + updateState { + it.copy( + genres = genres, + selectedGenre = null // 기본적으로 아무 장르도 선택하지 않음 + ) + } + } + .onFailure { + // 장르 로딩 실패는 조용히 처리 + } + } + } + + fun updateSearchQuery(query: String) { + updateState { it.copy(searchQuery = query) } + searchJob?.cancel() + loadMoreJob?.cancel() + + // 공백도 검색 가능하도록 수정 (빈 문자열만 제외) + if (query.isNotEmpty()) { + updateState { + it.copy( + isInitial = false, + isLiveSearching = true, + isCompleteSearching = false + ) + } + searchJob = viewModelScope.launch { + delay(300) + performSearch(query, isLiveSearch = true) + } + } else { + clearSearchResults() + } + } + + fun onSearchButtonClick() { + val query = uiState.value.searchQuery + if (query.isNotEmpty()) { // 공백도 검색 가능 (빈 문자열만 제외) + searchJob?.cancel() + loadMoreJob?.cancel() + + updateState { + it.copy( + isInitial = false, + isLiveSearching = false, + isCompleteSearching = true + ) + } + viewModelScope.launch { + performSearch(query, isLiveSearch = false) + loadRecentSearches() + } + } + } + + fun updateSelectedGenre(genre: Genre?) { + updateState { it.copy(selectedGenre = genre) } + // 필터 변경 시 새로운 검색 수행 (공백도 허용) + if (uiState.value.searchQuery.isNotEmpty() && !uiState.value.isInitial) { + performSearchWithCurrentQuery() + } + } + + fun updateSortType(sort: String) { + updateState { it.copy(selectedSort = sort) } + // 정렬 변경 시 새로운 검색 수행 (공백도 허용) + if (uiState.value.searchQuery.isNotEmpty() && !uiState.value.isInitial) { + performSearchWithCurrentQuery() + } + } + + private fun performSearchWithCurrentQuery() { + val currentState = uiState.value + if (currentState.searchQuery.isNotEmpty()) { // 공백도 허용 + searchJob?.cancel() + loadMoreJob?.cancel() + searchJob = viewModelScope.launch { + performSearch(currentState.searchQuery, isLiveSearch = currentState.isLiveSearching) + } + } + } + + fun loadMoreRooms() { + val currentState = uiState.value + if (currentState.canLoadMore && currentState.searchQuery.isNotEmpty()) { // 공백도 허용 + loadMoreJob?.cancel() + loadMoreJob = viewModelScope.launch { + performLoadMore() + } + } + } + + private suspend fun performSearch(query: String, isLiveSearch: Boolean) { + val currentState = uiState.value + updateState { + it.copy( + isSearching = true, + error = null, + searchResults = emptyList(), + nextCursor = null + ) + } + + val category = currentState.selectedGenre?.apiCategory ?: "" + roomsRepository.searchRooms( + keyword = query, + category = category, + sort = currentState.selectedSort, + isFinalized = !isLiveSearch, + cursor = null + ) + .onSuccess { response -> + response?.let { searchResponse -> + updateState { + it.copy( + searchResults = searchResponse.roomList, + nextCursor = searchResponse.nextCursor, + hasMore = !searchResponse.isLast, + isSearching = false, + error = null + ) + } + } ?: run { + updateState { + it.copy( + searchResults = emptyList(), + isSearching = false, + isLiveSearching = false, + isCompleteSearching = false, + hasMore = false, + error = if (isLiveSearch) null else "검색 결과를 불러올 수 없습니다." + ) + } + } + } + .onFailure { throwable -> + updateState { + it.copy( + searchResults = emptyList(), + isSearching = false, + isLiveSearching = false, + isCompleteSearching = false, + error = if (isLiveSearch) null else (throwable.message ?: "검색 중 오류가 발생했습니다.") + ) + } + } + } + + private suspend fun performLoadMore() { + val currentState = uiState.value + + updateState { it.copy(isLoadingMore = true) } + + val category = currentState.selectedGenre?.apiCategory ?: "" + roomsRepository.searchRooms( + keyword = currentState.searchQuery, + category = category, + sort = currentState.selectedSort, + isFinalized = true, + cursor = currentState.nextCursor + ) + .onSuccess { response -> + response?.let { searchResponse -> + updateState { + it.copy( + searchResults = it.searchResults + searchResponse.roomList, + nextCursor = searchResponse.nextCursor, + hasMore = !searchResponse.isLast, + isLoadingMore = false, + error = null + ) + } + } ?: run { + updateState { + it.copy( + isLoadingMore = false, + hasMore = false, + error = "추가 결과를 불러올 수 없습니다." + ) + } + } + } + .onFailure { throwable -> + updateState { + it.copy( + isLoadingMore = false, + error = throwable.message ?: "추가 결과를 불러오는 중 오류가 발생했습니다." + ) + } + } + } + + fun loadRecentSearches() { + viewModelScope.launch { recentSearchRepository.getRecentSearches("ROOM") .onSuccess { response -> response?.let { recentSearchResponse -> @@ -51,30 +290,12 @@ class GroupSearchViewModel @Inject constructor( } updateState { - it.copy( - recentSearches = recentSearchResponse.recentSearchList, - isLoading = false, - error = null - ) - } - } ?: run { - updateState { - it.copy( - recentSearches = emptyList(), - isLoading = false, - error = null - ) + it.copy(recentSearches = recentSearchResponse.recentSearchList) } } } - .onFailure { throwable -> - updateState { - it.copy( - recentSearches = emptyList(), - isLoading = false, - error = throwable.message ?: "최근 검색어를 불러오는 중 오류가 발생했습니다." - ) - } + .onFailure { + // 최근 검색어 로딩 실패는 조용히 처리 } } } @@ -97,8 +318,33 @@ class GroupSearchViewModel @Inject constructor( deleteRecentSearch(recentSearchItem.recentSearchId) } } + + private fun clearSearchResults() { + searchJob?.cancel() + loadMoreJob?.cancel() + updateState { + it.copy( + searchQuery = "", + isInitial = true, + isLiveSearching = false, + isCompleteSearching = false, + searchResults = emptyList(), + nextCursor = null, + hasMore = true, + isSearching = false, + isLoadingMore = false, + error = null + ) + } + } fun refreshData() { - loadRecentSearches() + loadInitialData() + } + + override fun onCleared() { + super.onCleared() + searchJob?.cancel() + loadMoreJob?.cancel() } } \ No newline at end of file From d4af43ecd8ecc908c1eb7a7028b535a2db6eafb3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 19 Aug 2025 01:03:34 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[feat]:=20=EB=AA=A8=EC=9E=84=EB=B0=A9=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20API=20=ED=99=94=EB=A9=B4=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/GroupFilteredSearchResult.kt | 41 +++- .../search/component/GroupLiveSearchResult.kt | 44 +++- .../group/search/screen/GroupSearchScreen.kt | 225 ++++++++---------- .../navigator/navigations/GroupNavigation.kt | 14 +- 4 files changed, 184 insertions(+), 140 deletions(-) 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 72aec70c..bcb5065e 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 @@ -11,8 +11,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -37,7 +41,10 @@ fun GroupFilteredSearchResult( onGenreSelect: (Int) -> Unit, resultCount: Int, roomList: List, - onRoomClick: (GroupCardItemRoomData) -> Unit = {} + onRoomClick: (GroupCardItemRoomData) -> Unit = {}, + canLoadMore: Boolean = false, + isLoadingMore: Boolean = false, + onLoadMore: () -> Unit = {} ) { Column { GenreChipRow( @@ -71,7 +78,23 @@ fun GroupFilteredSearchResult( subText = stringResource(R.string.group_no_search_result2) ) } else { - LazyColumn { + val listState = rememberLazyListState() + + // 무한 스크롤 트리거 감지 + val shouldLoadMore by remember { + derivedStateOf { + val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull() + lastVisibleItem != null && lastVisibleItem.index >= roomList.size - 3 && canLoadMore + } + } + + LaunchedEffect(shouldLoadMore) { + if (shouldLoadMore) { + onLoadMore() + } + } + + LazyColumn(state = listState) { itemsIndexed(roomList) { index, room -> CardItemRoomSmall( title = room.title, @@ -93,6 +116,20 @@ fun GroupFilteredSearchResult( ) } } + + // 로딩 인디케이터 + if (isLoadingMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = colors.White) + } + } + } } } } 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 e3ccac6f..053e4dce 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 @@ -8,7 +8,14 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -20,9 +27,28 @@ import com.texthip.thip.ui.theme.ThipTheme.colors @Composable fun GroupLiveSearchResult( roomList: List, - onRoomClick: (GroupCardItemRoomData) -> Unit = {} + onRoomClick: (GroupCardItemRoomData) -> Unit = {}, + canLoadMore: Boolean = false, + isLoadingMore: Boolean = false, + onLoadMore: () -> Unit = {} ) { - LazyColumn { + val listState = rememberLazyListState() + + // 무한 스크롤 트리거 감지 + val shouldLoadMore by remember { + derivedStateOf { + val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull() + lastVisibleItem != null && lastVisibleItem.index >= roomList.size - 3 && canLoadMore + } + } + + LaunchedEffect(shouldLoadMore) { + if (shouldLoadMore) { + onLoadMore() + } + } + + LazyColumn(state = listState) { itemsIndexed(roomList) { index, room -> CardItemRoomSmall( title = room.title, @@ -44,6 +70,20 @@ fun GroupLiveSearchResult( ) } } + + // 로딩 인디케이터 + if (isLoadingMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = colors.White) + } + } + } } } 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 2ed38932..ecbe4529 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 @@ -10,13 +10,8 @@ import androidx.compose.foundation.layout.padding 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.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -37,67 +32,61 @@ import com.texthip.thip.ui.group.search.component.GroupFilteredSearchResult import com.texthip.thip.ui.group.search.component.GroupLiveSearchResult import com.texthip.thip.ui.group.search.viewmodel.GroupSearchViewModel import com.texthip.thip.ui.theme.ThipTheme +import com.texthip.thip.utils.rooms.DateUtils @Composable fun GroupSearchScreen( modifier: Modifier = Modifier, - roomList: List, onNavigateBack: () -> Unit = {}, - onRoomClick: (GroupCardItemRoomData) -> Unit = {}, + onRoomClick: (Int) -> Unit = {}, viewModel: GroupSearchViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() - var searchText by rememberSaveable { mutableStateOf("") } - var isSearched by rememberSaveable { mutableStateOf(false) } - var selectedGenreIndex by rememberSaveable { mutableIntStateOf(-1) } - var selectedSortOptionIndex by rememberSaveable { mutableIntStateOf(0) } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current - val 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 genreDisplayNames = uiState.genres.map { genre -> + when (genre.displayKey) { + "literature" -> stringResource(R.string.literature) + "science_it" -> stringResource(R.string.science_it) + "social_science" -> stringResource(R.string.social_science) + "humanities" -> stringResource(R.string.humanities) + "art" -> stringResource(R.string.art) + else -> genre.apiCategory + } + } + val sortOptions = listOf( stringResource(R.string.group_filter_deadline), stringResource(R.string.group_filter_popular) ) - - val liveFilteredRoomList by remember(searchText) { - derivedStateOf { - if (searchText.isBlank()) emptyList() else - roomList.filter { room -> - room.title.contains(searchText, ignoreCase = true) - } - } + + val selectedGenreIndex = if (uiState.selectedGenre != null) { + uiState.genres.indexOf(uiState.selectedGenre) + } else -1 + + val selectedSortOptionIndex = when (uiState.selectedSort) { + "deadline" -> 0 + "memberCount" -> 1 + else -> 0 } - val filteredRoomList by remember( - searchText, - selectedGenreIndex, - selectedSortOptionIndex, - isSearched - ) { - derivedStateOf { - if (!isSearched) emptyList() - else { - val filtered = roomList.filter { room -> - (searchText.isBlank() || room.title.contains(searchText, ignoreCase = true)) - } - when (selectedSortOptionIndex) { - 0 -> filtered.sortedBy { it.endDate } // 마감임박순 - 1 -> filtered.sortedByDescending { it.participants } // 인기순 - else -> filtered - } - } - } + // 검색 결과를 GroupCardItemRoomData로 변환 + val convertedRoomList = uiState.searchResults.map { searchRoomItem -> + GroupCardItemRoomData( + id = searchRoomItem.roomId, + title = searchRoomItem.roomName, + participants = searchRoomItem.memberCount, + maxParticipants = searchRoomItem.recruitCount, + isRecruiting = true, + endDate = DateUtils.extractDaysFromDeadline(searchRoomItem.deadlineDate), + imageUrl = searchRoomItem.bookImageUrl, + isSecret = !searchRoomItem.isPublic + ) } - LaunchedEffect(isSearched) { - if (isSearched) { + LaunchedEffect(uiState.isCompleteSearching) { + if (uiState.isCompleteSearching) { focusManager.clearFocus() } } @@ -124,80 +113,101 @@ fun GroupSearchScreen( .fillMaxWidth() .focusRequester(focusRequester), hint = stringResource(R.string.group_room_search_hint), - text = searchText, - onValueChange = { - searchText = it - isSearched = false + text = uiState.searchQuery, + onValueChange = { query -> + viewModel.updateSearchQuery(query) }, - onSearch = { query -> - // 검색 실행 - isSearched = true - selectedGenreIndex = -1 - // 최근 검색어 새로고침 (서버에서 자동으로 추가됨) - viewModel.refreshData() + onSearch = { + viewModel.onSearchButtonClick() } ) Spacer(modifier = Modifier.height(16.dp)) when { - searchText.isBlank() && !isSearched && uiState.recentSearches.isEmpty() -> { - GroupRecentSearch( - recentSearches = emptyList(), - onSearchClick = {}, - onRemove = {} - ) - } - - searchText.isBlank() && !isSearched && uiState.recentSearches.isNotEmpty() -> { - GroupRecentSearch( - recentSearches = uiState.recentSearches.map { it.searchTerm }, - onSearchClick = { keyword -> - searchText = keyword - isSearched = true - }, - onRemove = { keyword -> - viewModel.deleteRecentSearchByKeyword(keyword) - } - ) + uiState.isInitial -> { + if (uiState.recentSearches.isEmpty()) { + GroupRecentSearch( + recentSearches = emptyList(), + onSearchClick = {}, + onRemove = {} + ) + } else { + GroupRecentSearch( + recentSearches = uiState.recentSearches.map { it.searchTerm }, + onSearchClick = { keyword -> + viewModel.updateSearchQuery(keyword) + viewModel.onSearchButtonClick() + }, + onRemove = { keyword -> + viewModel.deleteRecentSearchByKeyword(keyword) + } + ) + } } - searchText.isNotBlank() && !isSearched -> { - if (liveFilteredRoomList.isEmpty()) { + uiState.isLiveSearching -> { + if (uiState.showEmptyState) { GroupEmptyResult( mainText = stringResource(R.string.group_no_search_result1), subText = stringResource(R.string.group_no_search_result2) ) - } else { + } else if (uiState.hasResults) { GroupLiveSearchResult( - roomList = liveFilteredRoomList, - onRoomClick = onRoomClick + roomList = convertedRoomList, + onRoomClick = { room -> onRoomClick(room.id) }, + canLoadMore = uiState.canLoadMore, + isLoadingMore = uiState.isLoadingMore, + onLoadMore = { viewModel.loadMoreRooms() } ) } } - isSearched -> { + uiState.isCompleteSearching -> { GroupFilteredSearchResult( - genres = genres, + genres = genreDisplayNames, selectedGenreIndex = selectedGenreIndex, - onGenreSelect = { selectedGenreIndex = it }, - resultCount = filteredRoomList.size, - roomList = filteredRoomList, - onRoomClick = onRoomClick + onGenreSelect = { index -> + val currentSelectedIndex = if (uiState.selectedGenre != null) { + uiState.genres.indexOf(uiState.selectedGenre) + } else -1 + + val selectedGenre = if (index == currentSelectedIndex) { + // 같은 장르를 다시 터치하면 선택 해제 + null + } else if (index >= 0 && index < uiState.genres.size) { + // 새로운 장르 선택 + uiState.genres[index] + } else { + null + } + viewModel.updateSelectedGenre(selectedGenre) + }, + resultCount = convertedRoomList.size, + roomList = convertedRoomList, + onRoomClick = { room -> onRoomClick(room.id) }, + canLoadMore = uiState.canLoadMore, + isLoadingMore = uiState.isLoadingMore, + onLoadMore = { viewModel.loadMoreRooms() } ) } } } } - if (isSearched) { + if (uiState.isCompleteSearching) { FilterButton( modifier = Modifier .align(Alignment.TopEnd) - .padding(top = 176.dp, end = 20.dp), + .padding(top = 196.dp, end = 20.dp), selectedOption = sortOptions[selectedSortOptionIndex], options = sortOptions, onOptionSelected = { selected -> - selectedSortOptionIndex = sortOptions.indexOf(selected) + val sortType = when (sortOptions.indexOf(selected)) { + 0 -> "deadline" + 1 -> "memberCount" + else -> "deadline" + } + viewModel.updateSortType(sortType) } ) } @@ -209,39 +219,6 @@ fun GroupSearchScreen( @Composable fun PreviewGroupSearchScreen() { ThipTheme { - GroupSearchScreen( - roomList = listOf( - GroupCardItemRoomData( - id = 1, - title = "aaa", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - imageUrl = null, - isSecret = false - ), - GroupCardItemRoomData( - id = 2, - title = "abc", - participants = 15, - maxParticipants = 20, - isRecruiting = true, - endDate = 7, - imageUrl = null, - isSecret = true - ), - GroupCardItemRoomData( - id = 3, - title = "abcd", - participants = 10, - maxParticipants = 15, - isRecruiting = true, - endDate = 5, - imageUrl = null, - isSecret = true - ) - ) - ) + GroupSearchScreen() } } 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 28320fff..fa07b2eb 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 @@ -171,22 +171,12 @@ fun NavGraphBuilder.groupNavigation( // Group Search 화면 composable { - val groupViewModel: GroupViewModel = hiltViewModel() - val uiState by groupViewModel.uiState.collectAsState() - GroupSearchScreen( - roomList = emptyList(), //TODO: RoomMainResponse -> GroupCardItemRoomData 변환 필요 onNavigateBack = { navigateBack() }, - onRoomClick = { room -> - if (room.isRecruiting) { - // TODO: GroupCardItemRoomData -> RoomMainResponse 변환 후 roomId 사용 - // navController.navigateToGroupRecruit(room.roomId) - } else { - // TODO: GroupCardItemRoomData -> RoomMainResponse 변환 후 roomId 사용 - // navController.navigateToGroupRoom(room.roomId) - } + onRoomClick = { roomId -> + navController.navigateToGroupRecruit(roomId) } ) } From 819f7f9820ca1dc01ae3616f25b85f3b8e9f4e6b Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 19 Aug 2025 02:00:55 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[refactor]:=20~=EC=9D=BC=20=EB=92=A4=20?= =?UTF-8?q?=EC=99=80=20=EA=B0=99=EC=9D=80=20response=EB=A5=BC=20=EA=B7=B8?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/common/cards/CardItemRoom.kt | 21 ++++----- .../thip/ui/common/cards/CardItemRoomSmall.kt | 9 ++-- .../component/GroupDeadlineRoomSection.kt | 24 ++++++----- .../myroom/mock/GroupCardItemRoomData.kt | 6 +-- .../ui/group/myroom/screen/GroupMyScreen.kt | 13 +++--- .../room/screen/GroupRoomRecruitScreen.kt | 43 +++++++++++-------- .../ui/search/screen/SearchBookGroupScreen.kt | 28 ++++++------ .../com/texthip/thip/utils/rooms/DateUtils.kt | 13 ------ .../com/texthip/thip/utils/rooms/RoomUtils.kt | 4 -- app/src/main/res/values/strings.xml | 5 ++- 10 files changed, 77 insertions(+), 89 deletions(-) delete mode 100644 app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt 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 179ea627..32b35d76 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 @@ -19,7 +19,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -39,7 +38,7 @@ fun CardItemRoom( participants: Int, maxParticipants: Int, isRecruiting: Boolean, - endDate: Int? = null, + endDate: String? = null, imageUrl: String? = null, hasBorder: Boolean = false, onClick: () -> Unit = {} @@ -83,8 +82,8 @@ fun CardItemRoom( Column( modifier = Modifier - .fillMaxWidth() - .height(107.dp), + .fillMaxWidth() + .height(107.dp), verticalArrangement = Arrangement.Center ) { Text( @@ -151,10 +150,8 @@ fun CardItemRoom( Spacer(modifier = Modifier.height(5.dp)) Text( - text = stringResource( - R.string.card_item_end_date, - endDate - ) + if (isRecruiting) stringResource( + text = endDate + + if (isRecruiting) stringResource( R.string.card_item_end ) else stringResource(R.string.card_item_finish), @@ -180,7 +177,7 @@ fun CardItemRoomPreview() { participants = 22, maxParticipants = 30, isRecruiting = true, - endDate = 3, + endDate = "3일 뒤", imageUrl = null ) CardItemRoom( @@ -188,7 +185,7 @@ fun CardItemRoomPreview() { participants = 22, maxParticipants = 30, isRecruiting = false, - endDate = 3, + endDate = "3", imageUrl = null ) CardItemRoom( @@ -196,7 +193,7 @@ fun CardItemRoomPreview() { participants = 22, maxParticipants = 30, isRecruiting = true, - endDate = 3, + endDate = "3", imageUrl = null, hasBorder = true ) @@ -205,7 +202,7 @@ fun CardItemRoomPreview() { participants = 22, maxParticipants = 30, isRecruiting = false, - endDate = 3, + endDate = "3", 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 f933f1b4..b7ef137b 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 @@ -21,7 +21,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -40,7 +39,7 @@ fun CardItemRoomSmall( title: String, participants: Int, maxParticipants: Int, - endDate: Int?, + endDate: String?, imageUrl: String? = null, // API에서 받은 이미지 URL isWide: Boolean = false, isSecret: Boolean = false, @@ -146,7 +145,7 @@ fun CardItemRoomSmall( endDate?.let { Text( - text = stringResource(R.string.card_item_end_date_recruit, endDate), + text = endDate + stringResource(R.string.card_item_end), color = colors.Red, style = typography.menu_sb600_s12_h20 ) @@ -167,14 +166,14 @@ fun CardItemRoomSmallPreview() { title = "방 제목입니다 방 제목입니다", participants = 22, maxParticipants = 30, - endDate = 3 + endDate = "3일 뒤" ) CardItemRoomSmall( title = "와이드 카드 fillMaxWidth", participants = 18, maxParticipants = 25, - endDate = 5, + endDate = "5", isWide = true ) } 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 69d81ff0..2442587e 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 @@ -27,15 +27,14 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview 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.data.model.rooms.response.RoomMainList import com.texthip.thip.data.manager.Genre +import com.texthip.thip.data.model.rooms.response.RoomMainList import com.texthip.thip.data.model.rooms.response.RoomMainResponse +import com.texthip.thip.ui.common.buttons.GenreChipRow +import com.texthip.thip.ui.common.cards.CardItemRoom 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.DateUtils import com.texthip.thip.utils.rooms.toDisplayStrings @SuppressLint("UnusedBoxWithConstraintsScope") @@ -68,11 +67,17 @@ fun GroupRoomDeadlineSection( // Genre enum을 현지화된 문자열로 변환 val genreStrings = Genre.entries.toDisplayStrings() - + // 마감 임박 방 목록과 인기 방 목록을 섹션으로 구성 val roomSections = listOf( - Pair(stringResource(R.string.room_section_deadline), roomMainList?.deadlineRoomList ?: emptyList()), - Pair(stringResource(R.string.room_section_popular), roomMainList?.popularRoomList ?: emptyList()) + Pair( + stringResource(R.string.room_section_deadline), + roomMainList?.deadlineRoomList ?: emptyList() + ), + Pair( + stringResource(R.string.room_section_popular), + roomMainList?.popularRoomList ?: emptyList() + ) ) val effectivePagerState = rememberPagerState( @@ -188,20 +193,19 @@ fun GroupRoomDeadlineSection( ) { rooms.forEach { room -> // RoomMainResponse를 CardItemRoom에 맞게 변환 - val daysLeft = DateUtils.extractDaysFromDeadline(room.deadlineDate) CardItemRoom( title = room.roomName, participants = room.memberCount, maxParticipants = room.recruitCount, isRecruiting = true, // RoomMainResponse에는 모집중인 방만 있음 - endDate = daysLeft, + endDate = room.deadlineDate, imageUrl = room.bookImageUrl, onClick = { onRoomClick(room) }, hasBorder = true, ) } } - + if (rooms.size < 4) { Spacer( modifier = Modifier 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 7c149e9f..a282151e 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 @@ -6,9 +6,7 @@ data class GroupCardItemRoomData( val participants: Int, val maxParticipants: Int, val isRecruiting: Boolean, - val endDate: Int? = null, // 남은 일 수 + val endDate: String? = null, // 마감 시간 텍스트 (예: "8시간 뒤") val imageUrl: String? = null, // API에서 받은 이미지 URL val isSecret: Boolean = false -) - - +) \ 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 59b70a28..86f4f002 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 @@ -47,7 +47,7 @@ fun GroupMyScreen( viewModel: GroupMyViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() - + GroupMyContent( uiState = uiState, onCardClick = onCardClick, @@ -69,7 +69,7 @@ fun GroupMyContent( onChangeRoomType: (RoomType) -> Unit = {} ) { val listState = rememberLazyListState() - + // 무한 스크롤 로직 val shouldLoadMore by remember(uiState.canLoadMore, uiState.isLoadingMore) { derivedStateOf { @@ -78,13 +78,13 @@ fun GroupMyContent( uiState.canLoadMore && !uiState.isLoadingMore && totalItems > 0 && lastVisibleIndex >= totalItems - 3 } } - + LaunchedEffect(shouldLoadMore) { if (shouldLoadMore) { onLoadMore() } } - + // Filter 상태를 val selectedStates = remember(uiState.currentRoomType) { when (uiState.currentRoomType) { @@ -102,7 +102,7 @@ fun GroupMyContent( title = stringResource(R.string.my_group_room), onLeftClick = onNavigateBack, ) - + PullToRefreshBox( isRefreshing = uiState.isLoading, onRefresh = onRefresh, @@ -138,6 +138,7 @@ fun GroupMyContent( RoomType.RECRUITING } } + else -> RoomType.PLAYING_AND_RECRUITING } onChangeRoomType(newRoomType) @@ -159,7 +160,7 @@ fun GroupMyContent( participants = room.memberCount, maxParticipants = room.recruitCount, isRecruiting = RoomUtils.isRecruitingByType(room.type), - endDate = RoomUtils.getEndDateInDays(room.endDate), + endDate = room.endDate, imageUrl = room.bookImageUrl, onClick = { onCardClick(room) } ) 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 e8ee0ad2..d3154c0c 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 @@ -1,7 +1,6 @@ package com.texthip.thip.ui.group.room.screen import androidx.compose.foundation.background -import coil.compose.AsyncImage import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -29,7 +28,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -38,6 +36,7 @@ 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 coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.data.model.rooms.response.RecommendRoomResponse import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse @@ -53,7 +52,6 @@ import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography import com.texthip.thip.utils.color.hexToColor -import com.texthip.thip.utils.rooms.DateUtils import kotlinx.coroutines.delay @Composable @@ -68,12 +66,12 @@ fun GroupRoomRecruitScreen( viewModel: GroupRoomRecruitViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() - + // 데이터 로딩 LaunchedEffect(roomId) { viewModel.loadRoomDetail(roomId) } - + // GroupScreen으로 네비게이션 LaunchedEffect(uiState.shouldNavigateToGroupScreen, uiState.toastMessage) { if (uiState.shouldNavigateToGroupScreen) { @@ -81,7 +79,7 @@ fun GroupRoomRecruitScreen( viewModel.onNavigatedToGroupScreen() } } - + // 기록장 화면으로 네비게이션 LaunchedEffect(uiState.shouldNavigateToRoomPlayingScreen, uiState.roomId) { if (uiState.shouldNavigateToRoomPlayingScreen) { @@ -92,13 +90,13 @@ fun GroupRoomRecruitScreen( } } } - + GroupRoomRecruitContent( uiState = uiState, onRecommendationClick = onRecommendationClick, onBackClick = onBackClick, onBookDetailClick = onBookDetailClick, - onParticipationClick = { + onParticipationClick = { // 비밀방이면 비밀번호 화면으로, 공개방이면 바로 참여 val detail = uiState.roomDetail if (detail != null && !detail.isPublic) { @@ -107,8 +105,18 @@ fun GroupRoomRecruitScreen( viewModel.onParticipationClick() } }, - onCancelParticipationClick = { title, description -> viewModel.onCancelParticipationClick(title, description) }, - onCloseRecruitmentClick = { title, description -> viewModel.onCloseRecruitmentClick(title, description) }, + onCancelParticipationClick = { title, description -> + viewModel.onCancelParticipationClick( + title, + description + ) + }, + onCloseRecruitmentClick = { title, description -> + viewModel.onCloseRecruitmentClick( + title, + description + ) + }, onDialogConfirm = { viewModel.onDialogConfirm() }, onDialogCancel = { viewModel.onDialogCancel() }, onHideToast = { viewModel.hideToast() } @@ -142,10 +150,10 @@ fun GroupRoomRecruitContent( } return@Box } - + // 데이터가 없는 경우 val detail = uiState.roomDetail ?: return@Box - + AsyncImage( model = detail.roomImageUrl, contentDescription = "모임방 배경 이미지", @@ -330,12 +338,10 @@ fun GroupRoomRecruitContent( 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, - daysLeft + text = detail.recruitEndDate.replace( + "뒤", + "남음" ), style = typography.info_m500_s12, color = colors.NeonGreen @@ -391,12 +397,11 @@ fun GroupRoomRecruitContent( horizontalArrangement = Arrangement.spacedBy(20.dp) ) { items(detail.recommendRooms) { rec -> - val daysLeft = DateUtils.extractDaysFromDeadline(rec.recruitEndDate) CardItemRoomSmall( title = rec.roomName, participants = rec.memberCount, maxParticipants = rec.recruitCount, - endDate = daysLeft, + endDate = rec.recruitEndDate, imageUrl = rec.roomImageUrl, onClick = { onRecommendationClick(rec) } ) 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 cf6fcea9..b8189f53 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 @@ -39,7 +39,6 @@ import com.texthip.thip.ui.search.viewmodel.SearchBookGroupViewModel 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.DateUtils @Composable @@ -57,14 +56,12 @@ fun SearchBookGroupScreen( } val recruitingList = uiState.recruitingRooms.map { item -> - val daysLeft = DateUtils.extractDaysFromDeadline(item.deadlineEndDate) - GroupCardItemRoomData( id = item.roomId, title = item.roomName, participants = item.memberCount, maxParticipants = item.recruitCount, - endDate = daysLeft, + endDate = item.deadlineEndDate, imageUrl = item.bookImageUrl, isRecruiting = true ) @@ -119,6 +116,7 @@ private fun SearchBookGroupScreenContent( ) } } + error != null -> { Box( modifier = modifier.fillMaxSize(), @@ -131,6 +129,7 @@ private fun SearchBookGroupScreenContent( ) } } + else -> { Box( modifier = modifier.fillMaxSize() @@ -195,21 +194,22 @@ private fun SearchBookGroupScreenContent( } } else { val listState = rememberLazyListState() - + // 무한 스크롤 로직 LaunchedEffect(listState, canLoadMore, isLoadingMore) { snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } .collect { lastVisibleIndex -> - if (lastVisibleIndex != null && + if (lastVisibleIndex != null && recruitingList.isNotEmpty() && !isLoadingMore && - lastVisibleIndex >= recruitingList.size - 3 && - canLoadMore) { + lastVisibleIndex >= recruitingList.size - 3 && + canLoadMore + ) { onLoadMore() } } } - + LazyColumn( state = listState, verticalArrangement = Arrangement.spacedBy(20.dp), @@ -227,7 +227,7 @@ private fun SearchBookGroupScreenContent( onClick = { onCardClick(item.id) } ) } - + // 로딩 인디케이터 if (isLoadingMore) { item { @@ -257,7 +257,7 @@ private fun SearchBookGroupScreenContent( .fillMaxWidth() .height(50.dp), shape = RoundedCornerShape(0.dp), - onClick = { + onClick = { onCreateRoomClick(isbn, bookTitle, bookImageUrl, bookAuthor) } ) { @@ -280,7 +280,7 @@ private val mockRecruitingList = listOf( participants = 8, maxParticipants = 12, isRecruiting = true, - endDate = 3, + endDate = "3", imageUrl = "https://example.com/demian.jpg", isSecret = false ), @@ -290,7 +290,7 @@ private val mockRecruitingList = listOf( participants = 15, maxParticipants = 20, isRecruiting = true, - endDate = 7, + endDate = "7", imageUrl = "https://example.com/demian.jpg", isSecret = true ), @@ -300,7 +300,7 @@ private val mockRecruitingList = listOf( participants = 5, maxParticipants = 10, isRecruiting = true, - endDate = 1, + endDate = "1", imageUrl = "https://example.com/demian.jpg", isSecret = false ) diff --git a/app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt b/app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt deleted file mode 100644 index f5579e11..00000000 --- a/app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.texthip.thip.utils.rooms - - -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/utils/rooms/RoomUtils.kt b/app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt index e206136f..a0729419 100644 --- a/app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt +++ b/app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt @@ -11,8 +11,4 @@ object RoomUtils { else -> false } } - - fun getEndDateInDays(endDate: String): Int { - return DateUtils.extractDaysFromDeadline(endDate) - } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3c73e2d6..07502002 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,13 +46,14 @@ 시간 전 변경 "%1$s일 뒤 " - 모집 마감 + " 모집 마감" + + 남음 종료 %1$s명 참여 %1$s / %1$s명 - %1$s일 뒤 모집 마감 도서 소개 %1$s 저 기록장 From 9794ecf772150c9e26fbfed60bb14b2376c5e702 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 19 Aug 2025 02:01:16 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[refactor]:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20response=20=EC=82=AC=EC=9A=A9=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/GroupFilteredSearchResult.kt | 60 +++++++-------- .../search/component/GroupLiveSearchResult.kt | 77 +++++++++---------- .../group/search/screen/GroupSearchScreen.kt | 45 +++-------- 3 files changed, 77 insertions(+), 105 deletions(-) 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 bcb5065e..fd87e1aa 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 @@ -29,7 +29,7 @@ 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.CardItemRoomSmall -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData +import com.texthip.thip.data.model.rooms.response.SearchRoomItem import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -40,8 +40,8 @@ fun GroupFilteredSearchResult( selectedGenreIndex: Int, onGenreSelect: (Int) -> Unit, resultCount: Int, - roomList: List, - onRoomClick: (GroupCardItemRoomData) -> Unit = {}, + roomList: List, + onRoomClick: (SearchRoomItem) -> Unit = {}, canLoadMore: Boolean = false, isLoadingMore: Boolean = false, onLoadMore: () -> Unit = {} @@ -79,7 +79,7 @@ fun GroupFilteredSearchResult( ) } else { val listState = rememberLazyListState() - + // 무한 스크롤 트리거 감지 val shouldLoadMore by remember { derivedStateOf { @@ -87,23 +87,23 @@ fun GroupFilteredSearchResult( lastVisibleItem != null && lastVisibleItem.index >= roomList.size - 3 && canLoadMore } } - + LaunchedEffect(shouldLoadMore) { if (shouldLoadMore) { onLoadMore() } } - + LazyColumn(state = listState) { itemsIndexed(roomList) { index, room -> CardItemRoomSmall( - title = room.title, - participants = room.participants, - maxParticipants = room.maxParticipants, - endDate = room.endDate, - imageUrl = room.imageUrl, + title = room.roomName, + participants = room.memberCount, + maxParticipants = room.recruitCount, + endDate = room.deadlineDate, + imageUrl = room.bookImageUrl, isWide = true, - isSecret = room.isSecret, + isSecret = !room.isPublic, onClick = { onRoomClick(room) } ) if (index < roomList.size - 1) { @@ -116,7 +116,7 @@ fun GroupFilteredSearchResult( ) } } - + // 로딩 인디케이터 if (isLoadingMore) { item { @@ -151,24 +151,22 @@ fun GroupFilteredSearchResultPreview() { onGenreSelect = { selectedGenre = it }, resultCount = 3, roomList = listOf( - GroupCardItemRoomData( - id = 1, - title = "해리포터 독서모임", - participants = 5, - maxParticipants = 10, - isRecruiting = true, - endDate = 7, - imageUrl = null, - isSecret = false - ), GroupCardItemRoomData( - id = 2, - title = "소설 읽기 모임", - participants = 8, - maxParticipants = 12, - isRecruiting = false, - endDate = 3, - imageUrl = null, - isSecret = true + SearchRoomItem( + roomId = 1, + roomName = "해리포터 독서모임", + memberCount = 5, + recruitCount = 10, + deadlineDate = "7일 뒤", + bookImageUrl = null, + isPublic = true + ), SearchRoomItem( + roomId = 2, + roomName = "소설 읽기 모임", + memberCount = 8, + recruitCount = 12, + deadlineDate = "3일 뒤", + bookImageUrl = null, + isPublic = false ) ) ) 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 053e4dce..6fd8db00 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 @@ -20,20 +20,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.texthip.thip.ui.common.cards.CardItemRoomSmall -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData +import com.texthip.thip.data.model.rooms.response.SearchRoomItem import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @Composable fun GroupLiveSearchResult( - roomList: List, - onRoomClick: (GroupCardItemRoomData) -> Unit = {}, + roomList: List, + onRoomClick: (SearchRoomItem) -> Unit = {}, canLoadMore: Boolean = false, isLoadingMore: Boolean = false, onLoadMore: () -> Unit = {} ) { val listState = rememberLazyListState() - + // 무한 스크롤 트리거 감지 val shouldLoadMore by remember { derivedStateOf { @@ -41,23 +41,23 @@ fun GroupLiveSearchResult( lastVisibleItem != null && lastVisibleItem.index >= roomList.size - 3 && canLoadMore } } - + LaunchedEffect(shouldLoadMore) { if (shouldLoadMore) { onLoadMore() } } - + LazyColumn(state = listState) { itemsIndexed(roomList) { index, room -> CardItemRoomSmall( - title = room.title, - participants = room.participants, - maxParticipants = room.maxParticipants, - endDate = room.endDate, - imageUrl = room.imageUrl, + title = room.roomName, + participants = room.memberCount, + maxParticipants = room.recruitCount, + endDate = room.deadlineDate, + imageUrl = room.bookImageUrl, isWide = true, - isSecret = room.isSecret, + isSecret = !room.isPublic, onClick = { onRoomClick(room) } ) if (index < roomList.size - 1) { @@ -70,7 +70,7 @@ fun GroupLiveSearchResult( ) } } - + // 로딩 인디케이터 if (isLoadingMore) { item { @@ -97,35 +97,32 @@ fun GroupLiveSearchResultPreview() { ) { GroupLiveSearchResult( roomList = listOf( - GroupCardItemRoomData( - id = 1, - title = "해리포터 독서모임", - participants = 5, - maxParticipants = 10, - isRecruiting = true, - endDate = 7, - imageUrl = null, - isSecret = false + SearchRoomItem( + roomId = 1, + roomName = "해리포터 독서모임", + memberCount = 5, + recruitCount = 10, + deadlineDate = "7일 뒤", + bookImageUrl = null, + isPublic = true ), - GroupCardItemRoomData( - id = 2, - title = "소설 읽기 모임", - participants = 8, - maxParticipants = 12, - isRecruiting = false, - endDate = 3, - imageUrl = null, - isSecret = true + SearchRoomItem( + roomId = 2, + roomName = "소설 읽기 모임", + memberCount = 8, + recruitCount = 12, + deadlineDate = "3일 뒤", + bookImageUrl = null, + isPublic = false ), - GroupCardItemRoomData( - id = 3, - title = "비즈니스 서적 스터디", - participants = 3, - maxParticipants = 8, - isRecruiting = true, - endDate = null, - imageUrl = null, - isSecret = false + SearchRoomItem( + roomId = 3, + roomName = "비즈니스 서적 스터디", + memberCount = 3, + recruitCount = 8, + deadlineDate = "모집 중", + bookImageUrl = null, + isPublic = true ) ) ) 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 ecbe4529..d844e3a9 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 @@ -26,13 +26,12 @@ import com.texthip.thip.ui.common.buttons.FilterButton import com.texthip.thip.ui.common.forms.SearchBookTextField import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.group.myroom.component.GroupRecentSearch -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.search.component.GroupEmptyResult import com.texthip.thip.ui.group.search.component.GroupFilteredSearchResult import com.texthip.thip.ui.group.search.component.GroupLiveSearchResult import com.texthip.thip.ui.group.search.viewmodel.GroupSearchViewModel import com.texthip.thip.ui.theme.ThipTheme -import com.texthip.thip.utils.rooms.DateUtils +import com.texthip.thip.utils.rooms.toDisplayStrings @Composable fun GroupSearchScreen( @@ -45,45 +44,23 @@ fun GroupSearchScreen( val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current - val genreDisplayNames = uiState.genres.map { genre -> - when (genre.displayKey) { - "literature" -> stringResource(R.string.literature) - "science_it" -> stringResource(R.string.science_it) - "social_science" -> stringResource(R.string.social_science) - "humanities" -> stringResource(R.string.humanities) - "art" -> stringResource(R.string.art) - else -> genre.apiCategory - } - } - + val genreDisplayNames = uiState.genres.toDisplayStrings() + val sortOptions = listOf( stringResource(R.string.group_filter_deadline), stringResource(R.string.group_filter_popular) ) - + val selectedGenreIndex = if (uiState.selectedGenre != null) { uiState.genres.indexOf(uiState.selectedGenre) } else -1 - + val selectedSortOptionIndex = when (uiState.selectedSort) { "deadline" -> 0 "memberCount" -> 1 else -> 0 } - // 검색 결과를 GroupCardItemRoomData로 변환 - val convertedRoomList = uiState.searchResults.map { searchRoomItem -> - GroupCardItemRoomData( - id = searchRoomItem.roomId, - title = searchRoomItem.roomName, - participants = searchRoomItem.memberCount, - maxParticipants = searchRoomItem.recruitCount, - isRecruiting = true, - endDate = DateUtils.extractDaysFromDeadline(searchRoomItem.deadlineDate), - imageUrl = searchRoomItem.bookImageUrl, - isSecret = !searchRoomItem.isPublic - ) - } LaunchedEffect(uiState.isCompleteSearching) { if (uiState.isCompleteSearching) { @@ -153,8 +130,8 @@ fun GroupSearchScreen( ) } else if (uiState.hasResults) { GroupLiveSearchResult( - roomList = convertedRoomList, - onRoomClick = { room -> onRoomClick(room.id) }, + roomList = uiState.searchResults, + onRoomClick = { room -> onRoomClick(room.roomId) }, canLoadMore = uiState.canLoadMore, isLoadingMore = uiState.isLoadingMore, onLoadMore = { viewModel.loadMoreRooms() } @@ -170,7 +147,7 @@ fun GroupSearchScreen( val currentSelectedIndex = if (uiState.selectedGenre != null) { uiState.genres.indexOf(uiState.selectedGenre) } else -1 - + val selectedGenre = if (index == currentSelectedIndex) { // 같은 장르를 다시 터치하면 선택 해제 null @@ -182,9 +159,9 @@ fun GroupSearchScreen( } viewModel.updateSelectedGenre(selectedGenre) }, - resultCount = convertedRoomList.size, - roomList = convertedRoomList, - onRoomClick = { room -> onRoomClick(room.id) }, + resultCount = uiState.searchResults.size, + roomList = uiState.searchResults, + onRoomClick = { room -> onRoomClick(room.roomId) }, canLoadMore = uiState.canLoadMore, isLoadingMore = uiState.isLoadingMore, onLoadMore = { viewModel.loadMoreRooms() } From 377e531f935d8ddca5b872afbb9bfcc22e0a1aa6 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 19 Aug 2025 02:07:50 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[refactor]:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20response=20=EC=82=AC=EC=9A=A9=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/search/screen/SearchBookGroupScreen.kt | 79 ++++++++----------- 1 file changed, 31 insertions(+), 48 deletions(-) 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 b8189f53..0f574785 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 @@ -34,7 +34,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.myroom.mock.GroupCardItemRoomData +import com.texthip.thip.data.model.book.response.RecruitingRoomItem import com.texthip.thip.ui.search.viewmodel.SearchBookGroupViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -55,22 +55,11 @@ fun SearchBookGroupScreen( viewModel.loadRecruitingRooms(isbn) } - val recruitingList = uiState.recruitingRooms.map { item -> - GroupCardItemRoomData( - id = item.roomId, - title = item.roomName, - participants = item.memberCount, - maxParticipants = item.recruitCount, - endDate = item.deadlineEndDate, - imageUrl = item.bookImageUrl, - isRecruiting = true - ) - } SearchBookGroupScreenContent( isLoading = uiState.isLoading, error = uiState.error, - recruitingList = recruitingList, + recruitingList = uiState.recruitingRooms, totalCount = uiState.totalCount, isLoadingMore = uiState.isLoadingMore, canLoadMore = uiState.canLoadMore, @@ -92,7 +81,7 @@ private fun SearchBookGroupScreenContent( modifier: Modifier = Modifier, isLoading: Boolean = false, error: String? = null, - recruitingList: List = emptyList(), + recruitingList: List = emptyList(), totalCount: Int = 0, isLoadingMore: Boolean = false, canLoadMore: Boolean = true, @@ -218,13 +207,13 @@ private fun SearchBookGroupScreenContent( ) { items(recruitingList) { item -> CardItemRoom( - title = item.title, - participants = item.participants, - maxParticipants = item.maxParticipants, - isRecruiting = item.isRecruiting, - endDate = item.endDate, - imageUrl = item.imageUrl, - onClick = { onCardClick(item.id) } + title = item.roomName, + participants = item.memberCount, + maxParticipants = item.recruitCount, + isRecruiting = true, + endDate = item.deadlineEndDate, + imageUrl = item.bookImageUrl, + onClick = { onCardClick(item.roomId) } ) } @@ -274,35 +263,29 @@ private fun SearchBookGroupScreenContent( // Preview용 Mock 데이터 private val mockRecruitingList = listOf( - GroupCardItemRoomData( - id = 1, - title = "데미안 함께 읽기 📚", - participants = 8, - maxParticipants = 12, - isRecruiting = true, - endDate = "3", - imageUrl = "https://example.com/demian.jpg", - isSecret = false + RecruitingRoomItem( + roomId = 1, + roomName = "데미안 함께 읽기 📚", + memberCount = 8, + recruitCount = 12, + deadlineEndDate = "3일 뒤", + bookImageUrl = "https://example.com/demian.jpg" ), - GroupCardItemRoomData( - id = 2, - title = "헤르만 헤세 작품 토론방", - participants = 15, - maxParticipants = 20, - isRecruiting = true, - endDate = "7", - imageUrl = "https://example.com/demian.jpg", - isSecret = true + RecruitingRoomItem( + roomId = 2, + roomName = "헤르만 헤세 작품 토론방", + memberCount = 15, + recruitCount = 20, + deadlineEndDate = "7일 뒤", + bookImageUrl = "https://example.com/demian.jpg" ), - GroupCardItemRoomData( - id = 3, - title = "클래식 문학 읽기 모임", - participants = 5, - maxParticipants = 10, - isRecruiting = true, - endDate = "1", - imageUrl = "https://example.com/demian.jpg", - isSecret = false + RecruitingRoomItem( + roomId = 3, + roomName = "클래식 문학 읽기 모임", + memberCount = 5, + recruitCount = 10, + deadlineEndDate = "1일 뒤", + bookImageUrl = "https://example.com/demian.jpg" ) ) From 7a3c03b0643921713274088f53c7bd66de4d880b Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Tue, 19 Aug 2025 02:46:55 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[refactor]:=20PR=EB=A6=AC=EB=B7=B0=EC=97=90?= =?UTF-8?q?=20=EB=A7=9E=EA=B2=8C=20=EC=88=98=EC=A0=95=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/component/GroupRecentSearch.kt | 5 +- .../group/search/screen/GroupSearchScreen.kt | 2 +- .../search/viewmodel/GroupSearchViewModel.kt | 75 ++++++++++--------- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt index 11d44197..d23b3c46 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.ui.group.myroom.component +package com.texthip.thip.ui.group.search.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -7,9 +7,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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 d844e3a9..b9d3bc81 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 @@ -25,7 +25,7 @@ import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.FilterButton import com.texthip.thip.ui.common.forms.SearchBookTextField import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar -import com.texthip.thip.ui.group.myroom.component.GroupRecentSearch +import com.texthip.thip.ui.group.search.component.GroupRecentSearch import com.texthip.thip.ui.group.search.component.GroupEmptyResult import com.texthip.thip.ui.group.search.component.GroupFilteredSearchResult import com.texthip.thip.ui.group.search.component.GroupLiveSearchResult diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt index b8a20a01..95966920 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt @@ -19,29 +19,29 @@ import javax.inject.Inject data class GroupSearchUiState( val searchQuery: String = "", - + // 상태 관리 단순화 - boolean 필드 사용 val isInitial: Boolean = true, val isLiveSearching: Boolean = false, val isCompleteSearching: Boolean = false, - + // 검색 결과 및 데이터 val searchResults: List = emptyList(), val recentSearches: List = emptyList(), val genres: List = emptyList(), - + // 필터링 상태 val selectedGenre: Genre? = null, val selectedSort: String = "deadline", // "deadline" 또는 "memberCount" - + // 로딩 상태 val isSearching: Boolean = false, val isLoadingMore: Boolean = false, - + // 페이징 정보 val nextCursor: String? = null, val hasMore: Boolean = true, - + // 에러/토스트 val error: String? = null, val showToast: Boolean = false, @@ -60,17 +60,17 @@ class GroupSearchViewModel @Inject constructor( private val _uiState = MutableStateFlow(GroupSearchUiState()) val uiState: StateFlow = _uiState.asStateFlow() - + private var searchJob: Job? = null private var loadMoreJob: Job? = null - + // Map 기반 빠른 최근 검색어 관리 private val recentSearchMap = mutableMapOf() init { loadInitialData() } - + private fun updateState(update: (GroupSearchUiState) -> GroupSearchUiState) { _uiState.update(update) } @@ -79,16 +79,16 @@ class GroupSearchViewModel @Inject constructor( loadGenres() loadRecentSearches() } - + private fun loadGenres() { viewModelScope.launch { roomsRepository.getGenres() .onSuccess { genres -> - updateState { + updateState { it.copy( genres = genres, selectedGenre = null // 기본적으로 아무 장르도 선택하지 않음 - ) + ) } } .onFailure { @@ -104,12 +104,12 @@ class GroupSearchViewModel @Inject constructor( // 공백도 검색 가능하도록 수정 (빈 문자열만 제외) if (query.isNotEmpty()) { - updateState { + updateState { it.copy( isInitial = false, isLiveSearching = true, isCompleteSearching = false - ) + ) } searchJob = viewModelScope.launch { delay(300) @@ -126,12 +126,12 @@ class GroupSearchViewModel @Inject constructor( searchJob?.cancel() loadMoreJob?.cancel() - updateState { + updateState { it.copy( isInitial = false, isLiveSearching = false, isCompleteSearching = true - ) + ) } viewModelScope.launch { performSearch(query, isLiveSearch = false) @@ -139,7 +139,7 @@ class GroupSearchViewModel @Inject constructor( } } } - + fun updateSelectedGenre(genre: Genre?) { updateState { it.copy(selectedGenre = genre) } // 필터 변경 시 새로운 검색 수행 (공백도 허용) @@ -147,7 +147,7 @@ class GroupSearchViewModel @Inject constructor( performSearchWithCurrentQuery() } } - + fun updateSortType(sort: String) { updateState { it.copy(selectedSort = sort) } // 정렬 변경 시 새로운 검색 수행 (공백도 허용) @@ -155,13 +155,13 @@ class GroupSearchViewModel @Inject constructor( performSearchWithCurrentQuery() } } - + private fun performSearchWithCurrentQuery() { val currentState = uiState.value if (currentState.searchQuery.isNotEmpty()) { // 공백도 허용 searchJob?.cancel() loadMoreJob?.cancel() - + searchJob = viewModelScope.launch { performSearch(currentState.searchQuery, isLiveSearch = currentState.isLiveSearching) } @@ -180,18 +180,22 @@ class GroupSearchViewModel @Inject constructor( private suspend fun performSearch(query: String, isLiveSearch: Boolean) { val currentState = uiState.value - updateState { + updateState { it.copy( + isInitial = false, + isLiveSearching = isLiveSearch, + isCompleteSearching = !isLiveSearch, isSearching = true, error = null, searchResults = emptyList(), - nextCursor = null - ) + nextCursor = null, + hasMore = true + ) } val category = currentState.selectedGenre?.apiCategory ?: "" roomsRepository.searchRooms( - keyword = query, + keyword = query, category = category, sort = currentState.selectedSort, isFinalized = !isLiveSearch, @@ -213,8 +217,8 @@ class GroupSearchViewModel @Inject constructor( it.copy( searchResults = emptyList(), isSearching = false, - isLiveSearching = false, - isCompleteSearching = false, + isLiveSearching = isLiveSearch, + isCompleteSearching = !isLiveSearch, hasMore = false, error = if (isLiveSearch) null else "검색 결과를 불러올 수 없습니다." ) @@ -226,9 +230,10 @@ class GroupSearchViewModel @Inject constructor( it.copy( searchResults = emptyList(), isSearching = false, - isLiveSearching = false, - isCompleteSearching = false, - error = if (isLiveSearch) null else (throwable.message ?: "검색 중 오류가 발생했습니다.") + isLiveSearching = isLiveSearch, + isCompleteSearching = !isLiveSearch, + error = if (isLiveSearch) null else (throwable.message + ?: "검색 중 오류가 발생했습니다.") ) } } @@ -236,7 +241,7 @@ class GroupSearchViewModel @Inject constructor( private suspend fun performLoadMore() { val currentState = uiState.value - + updateState { it.copy(isLoadingMore = true) } val category = currentState.selectedGenre?.apiCategory ?: "" @@ -279,7 +284,7 @@ class GroupSearchViewModel @Inject constructor( } fun loadRecentSearches() { - viewModelScope.launch { + viewModelScope.launch { recentSearchRepository.getRecentSearches("ROOM") .onSuccess { response -> response?.let { recentSearchResponse -> @@ -288,7 +293,7 @@ class GroupSearchViewModel @Inject constructor( recentSearchResponse.recentSearchList.forEach { item -> recentSearchMap[item.searchTerm] = item } - + updateState { it.copy(recentSearches = recentSearchResponse.recentSearchList) } @@ -311,14 +316,14 @@ class GroupSearchViewModel @Inject constructor( } } } - + /** 키워드로 빠른 최근 검색어 삭제 (Map 기반) */ fun deleteRecentSearchByKeyword(keyword: String) { recentSearchMap[keyword]?.let { recentSearchItem -> deleteRecentSearch(recentSearchItem.recentSearchId) } } - + private fun clearSearchResults() { searchJob?.cancel() loadMoreJob?.cancel() @@ -341,7 +346,7 @@ class GroupSearchViewModel @Inject constructor( fun refreshData() { loadInitialData() } - + override fun onCleared() { super.onCleared() searchJob?.cancel()