diff --git a/data/src/main/java/org/gdsc/data/LocalHistoryDataStore.kt b/data/src/main/java/org/gdsc/data/LocalHistoryDataStore.kt new file mode 100644 index 00000000..320d80d3 --- /dev/null +++ b/data/src/main/java/org/gdsc/data/LocalHistoryDataStore.kt @@ -0,0 +1,58 @@ +package org.gdsc.data + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +val Context.historyDataStore: DataStore by preferencesDataStore(name = "history") + +class LocalHistoryDataStore @Inject constructor(private val context: Context) { + private val searchedKeywordKey = stringPreferencesKey("accessToken") + + suspend fun updateSearchedKeyword(newKeyword: String): List { + val previous = getSearchedKeywordToken()?.toConvertedList() ?: emptyList() + val newHistories = (previous + newKeyword).distinct().toConvertedString() + updateSearchedKeywordToken(newHistories) + return newHistories.toConvertedList() + } + + suspend fun getSearchedKeyword() = getSearchedKeywordToken()?.toConvertedList() ?: emptyList() + + suspend fun deleteSearchedKeyword(targetKeyword: String): List { + val previous = getSearchedKeywordToken()?.toConvertedList() ?: emptyList() + val newHistories = previous.filter { it != targetKeyword }.toConvertedString() + updateSearchedKeywordToken(newHistories) + return newHistories.toConvertedList() + } + + suspend fun initSearchedKeyword(): List { + updateSearchedKeywordToken(emptyList().toConvertedString()) + return emptyList() + } + + private suspend fun getSearchedKeywordToken() = context.historyDataStore.data + .map { preferences -> + preferences[searchedKeywordKey] + }.first() + + private suspend fun updateSearchedKeywordToken(newKeywords: String) { + context.historyDataStore.edit { preferences -> + preferences[searchedKeywordKey] = newKeywords + } + } + + private fun List.toConvertedString(): String { + return this.toString().replace("[", "").replace("]", "") + } + + private fun String.toConvertedList(): List { + return this.split(",").map { it.trim() } + } + +} \ No newline at end of file diff --git a/data/src/main/java/org/gdsc/data/database/RestaurantByMapPagingSource.kt b/data/src/main/java/org/gdsc/data/database/RestaurantByMapPagingSource.kt index c8916526..2236af2c 100644 --- a/data/src/main/java/org/gdsc/data/database/RestaurantByMapPagingSource.kt +++ b/data/src/main/java/org/gdsc/data/database/RestaurantByMapPagingSource.kt @@ -1,18 +1,14 @@ package org.gdsc.data.database -import android.util.Log import androidx.paging.PagingSource import androidx.paging.PagingState -import androidx.room.PrimaryKey -import kotlinx.coroutines.CoroutineScope import org.gdsc.data.model.RegisteredRestaurantResponse -import org.gdsc.data.model.Response import org.gdsc.data.network.RestaurantAPI -import org.gdsc.domain.model.request.RestaurantSearchMapRequest +import org.gdsc.domain.model.request.RestaurantSearchRequest class RestaurantByMapPagingSource( private val api: RestaurantAPI, - private val restaurantSearchMapRequest: RestaurantSearchMapRequest, + private val restaurantSearchRequest: RestaurantSearchRequest, ): PagingSource() { override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: 1 @@ -20,7 +16,7 @@ class RestaurantByMapPagingSource( val items = api.getRestaurantLocationInfoByMap( page = page, size = params.loadSize, - restaurantSearchMapRequest = restaurantSearchMapRequest + restaurantSearchRequest = restaurantSearchRequest ) LoadResult.Page( data = items.data.restaurants, diff --git a/data/src/main/java/org/gdsc/data/database/RestaurantBySearchPagingSource.kt b/data/src/main/java/org/gdsc/data/database/RestaurantBySearchPagingSource.kt new file mode 100644 index 00000000..df5725c0 --- /dev/null +++ b/data/src/main/java/org/gdsc/data/database/RestaurantBySearchPagingSource.kt @@ -0,0 +1,35 @@ +package org.gdsc.data.database + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import org.gdsc.data.model.RegisteredRestaurantResponse +import org.gdsc.data.network.RestaurantAPI +import org.gdsc.domain.model.request.RestaurantSearchRequest + +class RestaurantBySearchPagingSource( + private val api: RestaurantAPI, + private val restaurantSearchRequest: RestaurantSearchRequest, +): PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 1 + return try { + val items = api.getRegisteredRestaurantsBySearch( + restaurantSearchRequest = restaurantSearchRequest + ) + LoadResult.Page( + data = items.data.restaurants, + prevKey = null, + nextKey = if (items.data.restaurants.isEmpty()) null else page + 1 + ) + } catch (e: Exception) { + return LoadResult.Error(e) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/org/gdsc/data/database/RestaurantMediator.kt b/data/src/main/java/org/gdsc/data/database/RestaurantMediator.kt index ff8455d8..a0bb3c7b 100644 --- a/data/src/main/java/org/gdsc/data/database/RestaurantMediator.kt +++ b/data/src/main/java/org/gdsc/data/database/RestaurantMediator.kt @@ -6,14 +6,14 @@ import androidx.paging.PagingState import androidx.paging.RemoteMediator import androidx.room.withTransaction import org.gdsc.data.network.RestaurantAPI -import org.gdsc.domain.model.request.RestaurantSearchMapRequest +import org.gdsc.domain.model.request.RestaurantSearchRequest import retrofit2.HttpException import java.io.IOException @OptIn(ExperimentalPagingApi::class) class RestaurantMediator( private val userId: Int, - private val restaurantSearchMapRequest: RestaurantSearchMapRequest, + private val restaurantSearchRequest: RestaurantSearchRequest, private val db: RestaurantDatabase, private val api: RestaurantAPI, ): RemoteMediator() { @@ -47,7 +47,7 @@ class RestaurantMediator( userId = userId, page = currentPageNumber, size = state.config.pageSize, - restaurantSearchMapRequest = restaurantSearchMapRequest + restaurantSearchRequest = restaurantSearchRequest ) val repos = response.data diff --git a/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSource.kt b/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSource.kt index 962cddb9..bb8ffcea 100644 --- a/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSource.kt +++ b/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSource.kt @@ -15,7 +15,6 @@ import org.gdsc.domain.model.RestaurantLocationInfo import org.gdsc.domain.model.UserLocation import org.gdsc.domain.model.request.ModifyRestaurantInfoRequest import org.gdsc.domain.model.request.RestaurantRegistrationRequest -import org.gdsc.domain.model.request.RestaurantSearchMapRequest import org.gdsc.domain.model.response.RestaurantInfoResponse interface RestaurantDataSource { @@ -43,6 +42,10 @@ interface RestaurantDataSource { userLocation: Location?, startLocation: Location?, endLocation: Location?, sortType: SortType, foodCategory: FoodCategory?, drinkPossibility: DrinkPossibility? ): Flow> + suspend fun getRegisteredRestaurantsBySearch(keyword: String?, userLocation: Location?): Flow> + + suspend fun getRegisteredRestaurantsBySearchWithLimitCount(keyword: String?, userLocation: Location?, limit: Int): List + suspend fun getRestaurantReviews(restaurantId: Int): ReviewPaging suspend fun postRestaurantReview(restaurantId: Int, reviewContent: String, reviewImages: List): Boolean diff --git a/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSourceImpl.kt b/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSourceImpl.kt index 56bd1719..2d61cf87 100644 --- a/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSourceImpl.kt +++ b/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSourceImpl.kt @@ -15,6 +15,7 @@ import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import org.gdsc.data.database.RegisteredRestaurant import org.gdsc.data.database.RestaurantByMapPagingSource +import org.gdsc.data.database.RestaurantBySearchPagingSource import org.gdsc.data.database.RestaurantDatabase import org.gdsc.data.database.RestaurantMediator import org.gdsc.data.database.ReviewPaging @@ -32,7 +33,7 @@ import org.gdsc.domain.model.RestaurantLocationInfo import org.gdsc.domain.model.UserLocation import org.gdsc.domain.model.request.ModifyRestaurantInfoRequest import org.gdsc.domain.model.request.RestaurantRegistrationRequest -import org.gdsc.domain.model.request.RestaurantSearchMapRequest +import org.gdsc.domain.model.request.RestaurantSearchRequest import org.gdsc.domain.model.response.RestaurantInfoResponse import retrofit2.HttpException import javax.inject.Inject @@ -148,10 +149,10 @@ class RestaurantDataSourceImpl @Inject constructor( isCanDrinkLiquor = isCanDrinkLiquor, ) - val restaurantSearchMapRequest = RestaurantSearchMapRequest(filter, locationData) + val restaurantSearchRequest = RestaurantSearchRequest(filter, locationData) val mediator = RestaurantMediator( userId = userId, - restaurantSearchMapRequest = restaurantSearchMapRequest, + restaurantSearchRequest = restaurantSearchRequest, db = db, api = restaurantAPI, ) @@ -204,7 +205,7 @@ class RestaurantDataSourceImpl @Inject constructor( foodCategory: FoodCategory?, drinkPossibility: DrinkPossibility? ): Flow> { - val restaurantSearchMapRequest = RestaurantSearchMapRequest( + val restaurantSearchRequest = RestaurantSearchRequest( userLocation = userLocation, startLocation = startLocation, endLocation = endLocation, @@ -229,11 +230,45 @@ class RestaurantDataSourceImpl @Inject constructor( ) { RestaurantByMapPagingSource( restaurantAPI, - restaurantSearchMapRequest + restaurantSearchRequest ) }.flow.cachedIn(coroutineScope) } + override suspend fun getRegisteredRestaurantsBySearch( + keyword: String?, userLocation: Location? + ): Flow> { + return Pager( + config = PagingConfig( + pageSize = 20, + enablePlaceholders = true + ) + ) { + RestaurantBySearchPagingSource( + restaurantAPI, + RestaurantSearchRequest( + keyword = keyword, + userLocation = userLocation + ) + ) + }.flow.cachedIn(coroutineScope) + } + + override suspend fun getRegisteredRestaurantsBySearchWithLimitCount( + keyword: String?, + userLocation: Location?, + limit: Int + ): List { + + return restaurantAPI. + getRegisteredRestaurantsBySearch( + RestaurantSearchRequest( + keyword = keyword, + userLocation = userLocation + ) + ).data.restaurants.take(limit) + } + override suspend fun getRestaurantReviews(restaurantId: Int): ReviewPaging { return restaurantAPI.getRestaurantReviews(restaurantId).data } diff --git a/data/src/main/java/org/gdsc/data/datasource/UserDataSource.kt b/data/src/main/java/org/gdsc/data/datasource/UserDataSource.kt index 5260ba2c..12b0c3cd 100644 --- a/data/src/main/java/org/gdsc/data/datasource/UserDataSource.kt +++ b/data/src/main/java/org/gdsc/data/datasource/UserDataSource.kt @@ -23,4 +23,12 @@ interface UserDataSource { suspend fun postUserLogout(refreshToken: String): String suspend fun postUserSignout(): String + + suspend fun getSearchedKeywords(): List + + suspend fun updateSearchedKeyword(newKeyword: String): List + + suspend fun deleteSearchedKeyword(targetKeyword: String): List + + suspend fun initSearchedKeyword(): List } \ No newline at end of file diff --git a/data/src/main/java/org/gdsc/data/datasource/UserDataSourceImpl.kt b/data/src/main/java/org/gdsc/data/datasource/UserDataSourceImpl.kt index 07f739d7..f485628c 100644 --- a/data/src/main/java/org/gdsc/data/datasource/UserDataSourceImpl.kt +++ b/data/src/main/java/org/gdsc/data/datasource/UserDataSourceImpl.kt @@ -1,6 +1,7 @@ package org.gdsc.data.datasource import okhttp3.MultipartBody +import org.gdsc.data.LocalHistoryDataStore import org.gdsc.data.network.UserAPI import org.gdsc.domain.model.Response import org.gdsc.domain.model.request.NicknameRequest @@ -11,7 +12,8 @@ import retrofit2.HttpException import javax.inject.Inject class UserDataSourceImpl @Inject constructor( - private val userAPI: UserAPI + private val userAPI: UserAPI, + private val localHistoryDataStore: LocalHistoryDataStore ) : UserDataSource { override suspend fun postNickname(nicknameRequest: NicknameRequest): NicknameResponse { return userAPI.postNickname(nicknameRequest).data @@ -69,4 +71,20 @@ class UserDataSourceImpl @Inject constructor( override suspend fun postUserSignout(): String { return userAPI.postUserSignOut().code } + + override suspend fun getSearchedKeywords(): List { + return localHistoryDataStore.getSearchedKeyword() + } + + override suspend fun updateSearchedKeyword(newKeyword: String): List { + return localHistoryDataStore.updateSearchedKeyword(newKeyword) + } + + override suspend fun deleteSearchedKeyword(targetKeyword: String): List { + return localHistoryDataStore.deleteSearchedKeyword(targetKeyword) + } + + override suspend fun initSearchedKeyword(): List { + return localHistoryDataStore.initSearchedKeyword() + } } \ No newline at end of file diff --git a/data/src/main/java/org/gdsc/data/di/DatabaseModule.kt b/data/src/main/java/org/gdsc/data/di/DatabaseModule.kt index ec27e5ab..0ee9bd7e 100644 --- a/data/src/main/java/org/gdsc/data/di/DatabaseModule.kt +++ b/data/src/main/java/org/gdsc/data/di/DatabaseModule.kt @@ -6,6 +6,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import org.gdsc.data.LocalHistoryDataStore import org.gdsc.data.database.RestaurantDatabase import javax.inject.Singleton @@ -24,4 +25,8 @@ class DatabaseModule { @Singleton @Provides fun provideRestaurantDao(database: RestaurantDatabase) = database.restaurantDao() + + @Singleton + @Provides + fun provideHistoryDataStore(@ApplicationContext context: Context) = LocalHistoryDataStore(context) } \ No newline at end of file diff --git a/data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt b/data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt index 2d5ca9ea..5088ea2b 100644 --- a/data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt +++ b/data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt @@ -7,7 +7,7 @@ import org.gdsc.data.database.ReviewPaging import org.gdsc.data.model.Response import org.gdsc.domain.model.RestaurantLocationInfo import org.gdsc.domain.model.UserLocation -import org.gdsc.domain.model.request.RestaurantSearchMapRequest +import org.gdsc.domain.model.request.RestaurantSearchRequest import org.gdsc.domain.model.request.ModifyRestaurantInfoRequest import org.gdsc.domain.model.response.RestaurantInfoResponse import org.gdsc.domain.model.response.RestaurantRegistrationResponse @@ -54,13 +54,18 @@ interface RestaurantAPI { @Part pictures: List, ): Response + @POST("api/v1/restaurant/search") + suspend fun getRegisteredRestaurantsBySearch( + @Body restaurantSearchRequest: RestaurantSearchRequest, + ): Response + @POST("api/v1/restaurant/search/{userid}") suspend fun getRegisteredRestaurants( @Path("userid") userId: Int, @Query("page") page: Int? = null, @Query("size") size: Int? = null, @Query("sort") sort: String? = null, - @Body restaurantSearchMapRequest: RestaurantSearchMapRequest, + @Body restaurantSearchRequest: RestaurantSearchRequest, ): Response @PUT("api/v1/restaurant") @@ -73,7 +78,7 @@ interface RestaurantAPI { @Query("page") page: Int? = null, @Query("size") size: Int? = null, @Query("sort") sort: Array? = null, - @Body restaurantSearchMapRequest: RestaurantSearchMapRequest, + @Body restaurantSearchRequest: RestaurantSearchRequest, ): Response @GET("/api/v1/restaurant/{recommendRestaurantId}/review") diff --git a/data/src/main/java/org/gdsc/data/repository/RestaurantRepositoryImpl.kt b/data/src/main/java/org/gdsc/data/repository/RestaurantRepositoryImpl.kt index a8cfa244..806cb306 100644 --- a/data/src/main/java/org/gdsc/data/repository/RestaurantRepositoryImpl.kt +++ b/data/src/main/java/org/gdsc/data/repository/RestaurantRepositoryImpl.kt @@ -31,7 +31,10 @@ class RestaurantRepositoryImpl @Inject constructor( return restaurantDataSource.getRestaurantLocationInfo(query, latitude, longitude, page) } - override suspend fun getRecommendRestaurantInfo(recommendRestaurantId: Int, userLocation: UserLocation): RestaurantInfoResponse { + override suspend fun getRecommendRestaurantInfo( + recommendRestaurantId: Int, + userLocation: UserLocation + ): RestaurantInfoResponse { return restaurantDataSource.getRecommendRestaurantInfo(recommendRestaurantId, userLocation) } @@ -48,7 +51,11 @@ class RestaurantRepositoryImpl @Inject constructor( } override suspend fun getRestaurants( - userId: Int, locationData: Location, sortType: SortType, foodCategory: FoodCategory, drinkPossibility: DrinkPossibility + userId: Int, + locationData: Location, + sortType: SortType, + foodCategory: FoodCategory, + drinkPossibility: DrinkPossibility ): Flow> { return restaurantDataSource.getRestaurants( userId, @@ -79,7 +86,8 @@ class RestaurantRepositoryImpl @Inject constructor( differenceInDistance = restaurant.differenceInDistance, ) restaurantTemp - }, result.totalElementsCount) + }, result.totalElementsCount + ) pagingTemp } } @@ -90,7 +98,12 @@ class RestaurantRepositoryImpl @Inject constructor( } override suspend fun getRestaurantsByMap( - userLocation: Location?, startLocation: Location?, endLocation: Location?, sortType: SortType, foodCategory: FoodCategory?, drinkPossibility: DrinkPossibility? + userLocation: Location?, + startLocation: Location?, + endLocation: Location?, + sortType: SortType, + foodCategory: FoodCategory?, + drinkPossibility: DrinkPossibility? ): Flow> { return restaurantDataSource.getRestaurantsByMap( userLocation, @@ -123,10 +136,70 @@ class RestaurantRepositoryImpl @Inject constructor( } } + override suspend fun getRegisteredRestaurantsBySearch( + keyword: String?, + userLocation: Location? + ): Flow> { + return restaurantDataSource.getRegisteredRestaurantsBySearch(keyword, userLocation) + .map { result -> + result.map { restaurant -> + RegisteredRestaurant( + id = restaurant.id, + name = restaurant.name, + placeUrl = restaurant.placeUrl, + phone = restaurant.phone, + address = restaurant.address, + roadAddress = restaurant.roadAddress, + x = restaurant.x, + y = restaurant.y, + restaurantImageUrl = restaurant.restaurantImageUrl, + introduce = restaurant.introduce, + category = restaurant.category, + userId = restaurant.id, + userNickName = restaurant.userNickName, + userProfileImageUrl = restaurant.userProfileImageUrl, + canDrinkLiquor = restaurant.canDrinkLiquor, + differenceInDistance = restaurant.differenceInDistance, + ) + } + } + } + override suspend fun getRestaurantReviews(restaurantId: Int): List { return restaurantDataSource.getRestaurantReviews(restaurantId).reviewList + } + override suspend fun getRegisteredRestaurantsBySearchWithLimitCount( + keyword: String?, + userLocation: Location?, + limit: Int + ): List { + return restaurantDataSource.getRegisteredRestaurantsBySearchWithLimitCount(keyword, userLocation, limit) + .map { restaurant -> + RegisteredRestaurant( + id = restaurant.id, + name = restaurant.name, + placeUrl = restaurant.placeUrl, + phone = restaurant.phone, + address = restaurant.address, + roadAddress = restaurant.roadAddress, + x = restaurant.x, + y = restaurant.y, + restaurantImageUrl = restaurant.restaurantImageUrl, + introduce = restaurant.introduce, + category = restaurant.category, + userId = restaurant.id, + userNickName = restaurant.userNickName, + userProfileImageUrl = restaurant.userProfileImageUrl, + canDrinkLiquor = restaurant.canDrinkLiquor, + differenceInDistance = restaurant.differenceInDistance, + ) + } + } + +} + override suspend fun postRestaurantReview( restaurantId: Int, reviewContent: String, @@ -134,4 +207,4 @@ class RestaurantRepositoryImpl @Inject constructor( ): Boolean { return restaurantDataSource.postRestaurantReview(restaurantId, reviewContent, reviewImages) } -} \ No newline at end of file +} diff --git a/data/src/main/java/org/gdsc/data/repository/UserRepositoryImpl.kt b/data/src/main/java/org/gdsc/data/repository/UserRepositoryImpl.kt index dabb5d35..912bac34 100644 --- a/data/src/main/java/org/gdsc/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/org/gdsc/data/repository/UserRepositoryImpl.kt @@ -44,4 +44,20 @@ class UserRepositoryImpl @Inject constructor( return userDataSource.postUserSignout() } + override suspend fun getSearchedKeywords(): List { + return userDataSource.getSearchedKeywords() + } + + override suspend fun updateSearchedKeyword(newKeyword: String): List { + return userDataSource.updateSearchedKeyword(newKeyword) + } + + override suspend fun deleteSearchedKeyword(targetKeyword: String): List { + return userDataSource.deleteSearchedKeyword(targetKeyword) + } + + override suspend fun initSearchedKeyword(): List { + return userDataSource.initSearchedKeyword() + } + } \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/model/GroupInfo.kt b/domain/src/main/java/org/gdsc/domain/model/GroupInfo.kt new file mode 100644 index 00000000..21989d6a --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/model/GroupInfo.kt @@ -0,0 +1,13 @@ +package org.gdsc.domain.model + +data class GroupInfo( + val groupBackgroundImageUrl: String, + val groupId: Int, + val groupIntroduce: String, + val groupName: String, + val groupProfileImageUrl: String, + val isSelected: Boolean, + val memberCnt: Int, + val privateGroup: Boolean, + val restaurantCnt: Int +) \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/model/request/RestaurantSearchMapRequest.kt b/domain/src/main/java/org/gdsc/domain/model/request/RestaurantSearchRequest.kt similarity index 76% rename from domain/src/main/java/org/gdsc/domain/model/request/RestaurantSearchMapRequest.kt rename to domain/src/main/java/org/gdsc/domain/model/request/RestaurantSearchRequest.kt index 59750c38..d35e2750 100644 --- a/domain/src/main/java/org/gdsc/domain/model/request/RestaurantSearchMapRequest.kt +++ b/domain/src/main/java/org/gdsc/domain/model/request/RestaurantSearchRequest.kt @@ -4,13 +4,15 @@ import com.google.gson.annotations.SerializedName import org.gdsc.domain.model.Filter import org.gdsc.domain.model.Location -data class RestaurantSearchMapRequest( +data class RestaurantSearchRequest( @SerializedName("filter") - val filter: Filter, + val filter: Filter? = null, @SerializedName("userLocation") val userLocation: Location? = null, @SerializedName("startLocation") val startLocation: Location? = null, @SerializedName("endLocation") val endLocation: Location? = null, + @SerializedName("keyword") + val keyword: String? = null, ) diff --git a/domain/src/main/java/org/gdsc/domain/model/response/GroupResponse.kt b/domain/src/main/java/org/gdsc/domain/model/response/GroupResponse.kt new file mode 100644 index 00000000..4ce060ef --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/model/response/GroupResponse.kt @@ -0,0 +1,13 @@ +package org.gdsc.domain.model.response + +data class GroupResponse( + val groupBackgroundImageUrl: String, + val groupId: Int, + val groupIntroduce: String, + val groupName: String, + val groupProfileImageUrl: String, + val isSelected: Boolean, + val memberCnt: Int, + val privateGroup: Boolean, + val restaurantCnt: Int +) \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/repository/RestaurantRepository.kt b/domain/src/main/java/org/gdsc/domain/repository/RestaurantRepository.kt index e6bb650a..77eb872f 100644 --- a/domain/src/main/java/org/gdsc/domain/repository/RestaurantRepository.kt +++ b/domain/src/main/java/org/gdsc/domain/repository/RestaurantRepository.kt @@ -41,8 +41,12 @@ interface RestaurantRepository { userLocation: Location?, startLocation: Location?, endLocation: Location?, sortType: SortType, foodCategory: FoodCategory?, drinkPossibility: DrinkPossibility? ): Flow> + suspend fun getRegisteredRestaurantsBySearch(keyword: String?, userLocation: Location?): Flow> + suspend fun getRestaurantReviews(restaurantId: Int): List + suspend fun getRegisteredRestaurantsBySearchWithLimitCount(keyword: String?, userLocation: Location?, limit: Int): List + suspend fun postRestaurantReview(restaurantId: Int, reviewContent: String, reviewImages: List): Boolean } \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/repository/UserRepository.kt b/domain/src/main/java/org/gdsc/domain/repository/UserRepository.kt index 7d0c872f..0a205e32 100644 --- a/domain/src/main/java/org/gdsc/domain/repository/UserRepository.kt +++ b/domain/src/main/java/org/gdsc/domain/repository/UserRepository.kt @@ -23,4 +23,12 @@ interface UserRepository { suspend fun postUserLogout(refreshToken: String): String suspend fun postUserSignout(): String + + suspend fun getSearchedKeywords(): List + + suspend fun updateSearchedKeyword(newKeyword: String): List + + suspend fun deleteSearchedKeyword(targetKeyword: String): List + + suspend fun initSearchedKeyword(): List } \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/usecase/GetRegisteredRestaurantUseCase.kt b/domain/src/main/java/org/gdsc/domain/usecase/GetRegisteredRestaurantUseCase.kt index 7eaafc51..fc0485d4 100644 --- a/domain/src/main/java/org/gdsc/domain/usecase/GetRegisteredRestaurantUseCase.kt +++ b/domain/src/main/java/org/gdsc/domain/usecase/GetRegisteredRestaurantUseCase.kt @@ -1,6 +1,5 @@ package org.gdsc.domain.usecase -import androidx.paging.PagingData import kotlinx.coroutines.flow.Flow import org.gdsc.domain.DrinkPossibility import org.gdsc.domain.FoodCategory @@ -8,7 +7,6 @@ import org.gdsc.domain.SortType import org.gdsc.domain.model.Location import org.gdsc.domain.model.PagingResult import org.gdsc.domain.model.RegisteredRestaurant -import org.gdsc.domain.model.request.RestaurantSearchMapRequest import org.gdsc.domain.repository.RestaurantRepository import javax.inject.Inject diff --git a/domain/src/main/java/org/gdsc/domain/usecase/GetRestaurantBySearchUseCase.kt b/domain/src/main/java/org/gdsc/domain/usecase/GetRestaurantBySearchUseCase.kt new file mode 100644 index 00000000..f74de716 --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/usecase/GetRestaurantBySearchUseCase.kt @@ -0,0 +1,20 @@ +package org.gdsc.domain.usecase + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.gdsc.domain.model.Location +import org.gdsc.domain.model.RegisteredRestaurant +import org.gdsc.domain.repository.RestaurantRepository +import javax.inject.Inject + +class GetRestaurantBySearchUseCase @Inject constructor( + private val restaurantRepository: RestaurantRepository +) { + suspend operator fun invoke( + keyword: String?, + userLocation: Location? + ): Flow> { + + return restaurantRepository.getRegisteredRestaurantsBySearch( keyword, userLocation) + } +} \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/usecase/GetRestaurantBySearchWithLimitCountUseCase.kt b/domain/src/main/java/org/gdsc/domain/usecase/GetRestaurantBySearchWithLimitCountUseCase.kt new file mode 100644 index 00000000..5e24719f --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/usecase/GetRestaurantBySearchWithLimitCountUseCase.kt @@ -0,0 +1,19 @@ +package org.gdsc.domain.usecase + +import org.gdsc.domain.model.Location +import org.gdsc.domain.model.RegisteredRestaurant +import org.gdsc.domain.repository.RestaurantRepository +import javax.inject.Inject + +class GetRestaurantBySearchWithLimitCountUseCase @Inject constructor( + private val restaurantRepository: RestaurantRepository +) { + suspend operator fun invoke( + keyword: String?, + userLocation: Location?, + limit: Int + ): List { + + return restaurantRepository.getRegisteredRestaurantsBySearchWithLimitCount(keyword, userLocation, limit) + } +} \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/usecase/user/DeleteSearchedKeywordUseCase.kt b/domain/src/main/java/org/gdsc/domain/usecase/user/DeleteSearchedKeywordUseCase.kt new file mode 100644 index 00000000..bfc3e479 --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/usecase/user/DeleteSearchedKeywordUseCase.kt @@ -0,0 +1,13 @@ +package org.gdsc.domain.usecase.user + +import org.gdsc.domain.repository.UserRepository +import javax.inject.Inject + +class DeleteSearchedKeywordUseCase @Inject constructor( + private val userRepository: UserRepository +) { + + suspend operator fun invoke(keyword: String): List { + return userRepository.deleteSearchedKeyword(keyword) + } +} \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/usecase/user/GetSearchedKeywordsUseCase.kt b/domain/src/main/java/org/gdsc/domain/usecase/user/GetSearchedKeywordsUseCase.kt new file mode 100644 index 00000000..fb383b35 --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/usecase/user/GetSearchedKeywordsUseCase.kt @@ -0,0 +1,12 @@ +package org.gdsc.domain.usecase.user + +import org.gdsc.domain.repository.UserRepository +import javax.inject.Inject + +class GetSearchedKeywordsUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(): List { + return userRepository.getSearchedKeywords() + } +} \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/usecase/user/InitSearchedKeywordUseCase.kt b/domain/src/main/java/org/gdsc/domain/usecase/user/InitSearchedKeywordUseCase.kt new file mode 100644 index 00000000..a47fe068 --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/usecase/user/InitSearchedKeywordUseCase.kt @@ -0,0 +1,10 @@ +package org.gdsc.domain.usecase.user + +import org.gdsc.domain.repository.UserRepository +import javax.inject.Inject + +class InitSearchedKeywordUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke() = userRepository.initSearchedKeyword() +} \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/usecase/user/UpdateSearchedKeywordUseCase.kt b/domain/src/main/java/org/gdsc/domain/usecase/user/UpdateSearchedKeywordUseCase.kt new file mode 100644 index 00000000..f80374ed --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/usecase/user/UpdateSearchedKeywordUseCase.kt @@ -0,0 +1,13 @@ +package org.gdsc.domain.usecase.user + +import org.gdsc.domain.repository.UserRepository +import javax.inject.Inject + +class UpdateSearchedKeywordUseCase @Inject constructor( + private val userRepository: UserRepository +) { + + suspend operator fun invoke(keyword: String): List { + return userRepository.updateSearchedKeyword(keyword) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/base/SearchViewListener.kt b/presentation/src/main/java/org/gdsc/presentation/base/SearchViewListener.kt new file mode 100644 index 00000000..bd2bacfd --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/base/SearchViewListener.kt @@ -0,0 +1,12 @@ +package org.gdsc.presentation.base + +interface SearchViewListener { + fun changeFocus(focus: Boolean) + fun onChangeText(text: CharSequence) + fun onSubmitText(text: CharSequence) + fun onSearchClear() +} + +interface CancelViewListener{ + fun onCancel() +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchContainerFragment.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchContainerFragment.kt new file mode 100644 index 00000000..8d59bbf8 --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchContainerFragment.kt @@ -0,0 +1,98 @@ +package org.gdsc.presentation.view.allsearch + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.tabs.TabLayoutMediator +import dagger.hilt.android.AndroidEntryPoint +import org.gdsc.presentation.R +import org.gdsc.presentation.base.CancelViewListener +import org.gdsc.presentation.base.SearchViewListener +import org.gdsc.presentation.databinding.FragmentAllSearchContainerBinding +import org.gdsc.presentation.utils.repeatWhenUiStarted +import org.gdsc.presentation.view.MainActivity +import org.gdsc.presentation.view.allsearch.adapter.SearchCategoryPagerAdapter + +@AndroidEntryPoint +class AllSearchContainerFragment: Fragment() { + + private var _binding: FragmentAllSearchContainerBinding? = null + private val binding get() = _binding!! + + private val parent by lazy { requireActivity() as MainActivity } + + val viewModel: AllSearchViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAllSearchContainerBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + parent.changeToolbarVisible(false) + + binding.searchBar.setSearchViewListener(searchListener) + binding.searchBar.setCancelViewListener(cancelViewListener) + + arguments?.getString("keyword")?.let { + viewModel.setSearchKeyword(it) + binding.searchBar.setSearchText(it) + } + + setPager() + setTabLayout() + } + + private fun setPager() { + binding.searchCategoryPager.adapter = SearchCategoryPagerAdapter( + this, + viewModel.searchKeyword.value + ) + repeatWhenUiStarted { + viewModel.searchKeyword.collect { + binding.searchCategoryPager.adapter = SearchCategoryPagerAdapter(this, it) + } + } + } + + private fun setTabLayout() { + TabLayoutMediator(binding.tabLayout, binding.searchCategoryPager) { tab, position -> + when (position) { + SearchCategoryPagerAdapter.CATEGORY_ALL -> tab.text = getString(R.string.search_category_all) + SearchCategoryPagerAdapter.CATEGORY_RESTAURANT -> tab.text = getString(R.string.search_category_restaurant) + SearchCategoryPagerAdapter.CATEGORY_GROUP -> tab.text = getString(R.string.search_category_group) + } + }.attach() + } + + private val searchListener = object : SearchViewListener { + override fun onSearchClear() {} + override fun changeFocus(focus: Boolean) {} + override fun onChangeText(text: CharSequence) {} + override fun onSubmitText(text: CharSequence) { + if (text.isEmpty()) return + viewModel.setSearchKeyword(text.toString()) + binding.searchBar.setSearchText(text.toString()) + } + } + private val cancelViewListener = object : CancelViewListener { + override fun onCancel() { + findNavController().navigateUp() + } + } + + override fun onDestroyView() { + _binding = null + parent.changeToolbarVisible(true) + super.onDestroyView() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchFragment.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchFragment.kt new file mode 100644 index 00000000..90835c1b --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchFragment.kt @@ -0,0 +1,145 @@ +package org.gdsc.presentation.view.allsearch + +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.chip.Chip +import dagger.hilt.android.AndroidEntryPoint +import org.gdsc.presentation.R +import org.gdsc.presentation.base.CancelViewListener +import org.gdsc.presentation.base.SearchViewListener +import org.gdsc.presentation.databinding.FragmentAllSearchBinding +import org.gdsc.presentation.utils.repeatWhenUiStarted +import org.gdsc.presentation.view.MainActivity + +@AndroidEntryPoint +class AllSearchFragment : Fragment() { + + private var _binding: FragmentAllSearchBinding? = null + private val binding get() = _binding!! + + private val parent by lazy { requireActivity() as MainActivity } + + private val viewModel: AllSearchViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAllSearchBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + parent.changeToolbarTitle("검색") + + binding.searchBar.setSearchViewListener(searchListener) + binding.searchBar.setCancelViewListener(cancelViewListener) + + binding.tvDelete.setOnClickListener { + viewModel.deleteAllSearchedKeyword() + } + + repeatWhenUiStarted { + viewModel.searchedKeywordsState.collect { keywordList -> + binding.cgRecentSearch.removeAllViews() + keywordList.forEach { + if (it.isNotBlank()) { + binding.cgRecentSearch.addView( + newChip(it, + { keyword -> + viewModel.deleteSearchedKeyword(keyword) + }) { keyword -> + binding.searchBar.editText.setText(keyword) + navigateToResultPage() + } + ) + } + } + } + } + } + + private val searchListener = object : SearchViewListener { + override fun onSearchClear() {} + override fun changeFocus(focus: Boolean) {} + override fun onChangeText(text: CharSequence) {} + override fun onSubmitText(text: CharSequence) { + if (text.isEmpty()) return + viewModel.updateSearchedKeyword(text.toString()) + val action = + AllSearchFragmentDirections.actionAllSearchFragmentToAllSearchContainerFragment(text.toString()) + findNavController().navigate(action) + } + } + + private val cancelViewListener = object : CancelViewListener { + override fun onCancel() { + findNavController().navigateUp() + } + } + + private fun newChip( + text: String, + onCloseIconClicked: (String) -> Unit, + onClicked: (String) -> Unit + ): Chip { + return Chip(requireContext()).apply { + this.text = text + isCloseIconVisible = true + closeIcon = ContextCompat.getDrawable( + requireContext(), + R.drawable.cancel_icon + ) + + closeIconTint = ContextCompat.getColorStateList( + requireContext(), + R.color.grey200 + ) + + chipBackgroundColor = ContextCompat.getColorStateList( + requireContext(), + R.color.white + ) + chipStrokeColor = ContextCompat.getColorStateList( + requireContext(), + R.color.grey200 + ) + chipStrokeWidth = 1f + + rippleColor = ColorStateList.valueOf(Color.TRANSPARENT) + + setOnCloseIconClickListener { + onCloseIconClicked(text) + } + + setOnClickListener { + onClicked(text) + } + + } + + } + + private fun navigateToResultPage() { + val action = + AllSearchFragmentDirections.actionAllSearchFragmentToAllSearchContainerFragment(binding.searchBar.text) + findNavController().navigate(action) + } + + override fun onDestroyView() { + _binding = null + parent.changeToolbarTitle("") + super.onDestroyView() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchViewModel.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchViewModel.kt new file mode 100644 index 00000000..7ba86fa8 --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/AllSearchViewModel.kt @@ -0,0 +1,144 @@ +package org.gdsc.presentation.view.allsearch + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.gdsc.domain.DrinkPossibility +import org.gdsc.domain.FoodCategory +import org.gdsc.domain.SortType +import org.gdsc.domain.model.Location +import org.gdsc.domain.model.RegisteredRestaurant +import org.gdsc.domain.usecase.GetRestaurantBySearchUseCase +import org.gdsc.domain.usecase.GetRestaurantBySearchWithLimitCountUseCase +import org.gdsc.domain.usecase.user.DeleteSearchedKeywordUseCase +import org.gdsc.domain.usecase.user.GetSearchedKeywordsUseCase +import org.gdsc.domain.usecase.user.InitSearchedKeywordUseCase +import org.gdsc.domain.usecase.user.UpdateSearchedKeywordUseCase +import org.gdsc.presentation.JmtLocationManager +import javax.inject.Inject + +@HiltViewModel +class AllSearchViewModel @Inject constructor( + private val locationManager: JmtLocationManager, + private val getRestaurantBySearchUseCase: GetRestaurantBySearchUseCase, + private val getSearchedKeywordsUseCase: GetSearchedKeywordsUseCase, + private val updateSearchedKeywordUseCase: UpdateSearchedKeywordUseCase, + private val deleteSearchedKeywordUseCase: DeleteSearchedKeywordUseCase, + private val initSearchedKeywordUseCase: InitSearchedKeywordUseCase, + private val getRestaurantBySearchWithLimitCountUseCase: GetRestaurantBySearchWithLimitCountUseCase +) : ViewModel() { + + init { + + viewModelScope.launch { + val location = locationManager.getCurrentLocation() + + if (location == null) { + _searchedRestaurantPreviewState.value = emptyList() + } else { + + val userLoc = Location(location.longitude.toString(), location.latitude.toString()) + + _searchedRestaurantPreviewState.value = + getRestaurantBySearchWithLimitCountUseCase(searchKeyword.value, userLoc, 3) + } + + } + + viewModelScope.launch { + val location = locationManager.getCurrentLocation() + + if (location == null) { + _searchedRestaurantState.value = PagingData.empty() + } else { + val userLoc = Location(location.longitude.toString(), location.latitude.toString()) + + getRestaurantBySearchUseCase(searchKeyword.value, userLoc).distinctUntilChanged() + .collect { + _searchedRestaurantState.value = it + } + } + } + + viewModelScope.launch { + val keywords = getSearchedKeywordsUseCase() + if (keywords.isNotEmpty()) { + _searchedKeywordsState.value = keywords + } + } + } + + private var _searchKeyword = MutableStateFlow("") + val searchKeyword: StateFlow + get() = _searchKeyword + + private var _sortTypeState = MutableStateFlow(SortType.DISTANCE) + val sortTypeState: StateFlow + get() = _sortTypeState + + private var _foodCategoryState = MutableStateFlow(FoodCategory.INIT) + val foodCategoryState: StateFlow + get() = _foodCategoryState + + private var _drinkPossibilityState = MutableStateFlow(DrinkPossibility.INIT) + val drinkPossibilityState: StateFlow + get() = _drinkPossibilityState + + private var _searchedRestaurantState = + MutableStateFlow>(PagingData.empty()) + val searchedRestaurantState: StateFlow> + get() = _searchedRestaurantState + + private var _searchedRestaurantPreviewState = + MutableStateFlow>(emptyList()) + val searchedRestaurantPreviewState: StateFlow> + get() = _searchedRestaurantPreviewState + + + private var _searchedKeywordsState = MutableStateFlow>(emptyList()) + val searchedKeywordsState: StateFlow> + get() = _searchedKeywordsState + + fun setSearchKeyword(keyword: String) { + _searchKeyword.value = keyword + } + + fun setSortType(sortType: SortType) { + _sortTypeState.value = sortType + } + + fun setFoodCategory(foodCategory: FoodCategory) { + _foodCategoryState.value = foodCategory + } + + fun setDrinkPossibility(drinkPossibility: DrinkPossibility) { + _drinkPossibilityState.value = drinkPossibility + } + + fun deleteSearchedKeyword(keyword: String) { + viewModelScope.launch { + _searchedKeywordsState.value = deleteSearchedKeywordUseCase(keyword) + } + } + + fun updateSearchedKeyword(keyword: String) { + viewModelScope.launch { + _searchedKeywordsState.value = updateSearchedKeywordUseCase(keyword) + } + } + + fun deleteAllSearchedKeyword() { + viewModelScope.launch { + _searchedKeywordsState.value = initSearchedKeywordUseCase() + } + } + + +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryAllFragment.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryAllFragment.kt new file mode 100644 index 00000000..3c4ee35e --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryAllFragment.kt @@ -0,0 +1,115 @@ +package org.gdsc.presentation.view.allsearch + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.viewpager2.widget.ViewPager2 +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import org.gdsc.domain.model.GroupInfo +import org.gdsc.presentation.R +import org.gdsc.presentation.databinding.FragmentSearchCategoryAllBinding +import org.gdsc.presentation.utils.repeatWhenUiStarted +import org.gdsc.presentation.view.allsearch.adapter.SearchCategoryGroupPreviewAdapter +import org.gdsc.presentation.view.allsearch.adapter.SearchCategoryRestaurantAdapter +import org.gdsc.presentation.view.allsearch.adapter.SearchCategoryRestaurantPreviewAdapter + +@AndroidEntryPoint +class SearchCategoryAllFragment( + private val searchKeyword: String +): Fragment() { + + private var _binding: FragmentSearchCategoryAllBinding? = null + private val binding get() = _binding!! + + val viewModel: AllSearchViewModel by activityViewModels() + + private val searchCategoryRestaurantPreviewAdapter = SearchCategoryRestaurantPreviewAdapter() + private val searchCategoryGroupPreviewAdapter = SearchCategoryGroupPreviewAdapter() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSearchCategoryAllBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.restaurantRecyclerView.adapter = searchCategoryRestaurantPreviewAdapter + binding.restaurantRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + + binding.groupRecyclerView.adapter = searchCategoryGroupPreviewAdapter + binding.groupRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + + // Todo: New Adapter With Real APi + binding.recommendedRestaurantRecyclerView.adapter = searchCategoryRestaurantPreviewAdapter + binding.recommendedRestaurantRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + + binding.icRestaurant.setOnClickListener { + val viewPager = requireActivity().findViewById(R.id.search_category_pager) + viewPager.currentItem = 1 + } + + viewModel.setSearchKeyword(searchKeyword) + observeState() + } + private fun observeState() { + repeatWhenUiStarted { + viewModel.searchedRestaurantPreviewState.collect { + searchCategoryRestaurantPreviewAdapter.submitList(it) + } + } + + // Todo: Real APi + searchCategoryGroupPreviewAdapter.submitList( + listOf( + GroupInfo( + "https://avatars.githubusercontent.com/u/58663494?v=4", + 1, + "햄버거 먹으러 갈 사람 여기여기 모여라", + "버거 대마왕", + "https://avatars.githubusercontent.com/u/58663494?v=4", + false, + (0 .. 500).shuffled().first(), + false, + (0 .. 500).shuffled().first() + ), + GroupInfo( + "https://avatars.githubusercontent.com/u/58663494?v=4", + 2, + "햄버거 먹으러 갈 사람 여기여기 모여라", + "버거 대마왕", + "https://avatars.githubusercontent.com/u/58663494?v=4", + false, + (0 .. 500).shuffled().first(), + false, + (0 .. 500).shuffled().first() + ), + GroupInfo( + "https://avatars.githubusercontent.com/u/58663494?v=4", + 3, + "햄버거 먹으러 갈 사람 여기여기 모여라", + "버거 대마왕", + "https://avatars.githubusercontent.com/u/58663494?v=4", + false, + (0 .. 500).shuffled().first(), + false, + (0 .. 500).shuffled().first() + ), + ) + ) + } + + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryGroupFragment.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryGroupFragment.kt new file mode 100644 index 00000000..487d0ff7 --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryGroupFragment.kt @@ -0,0 +1,31 @@ +package org.gdsc.presentation.view.allsearch + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import org.gdsc.presentation.databinding.FragmentSearchCategoryGroupBinding + +class SearchCategoryGroupFragment( + private val searchKeyword: String +): Fragment() { + + private var _binding: FragmentSearchCategoryGroupBinding? = null + private val binding get() = _binding!! + + private val viewModel: AllSearchViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSearchCategoryGroupBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryRestaurantFragment.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryRestaurantFragment.kt new file mode 100644 index 00000000..cd13104f --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/SearchCategoryRestaurantFragment.kt @@ -0,0 +1,101 @@ +package org.gdsc.presentation.view.allsearch + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest +import org.gdsc.domain.DrinkPossibility +import org.gdsc.domain.FoodCategory +import org.gdsc.domain.SortType +import org.gdsc.presentation.databinding.FragmentSearchCategoryRestaurantBinding +import org.gdsc.presentation.utils.repeatWhenUiStarted +import org.gdsc.presentation.view.allsearch.adapter.SearchCategoryRestaurantAdapter + +@AndroidEntryPoint +class SearchCategoryRestaurantFragment( + private val searchKeyword: String +): Fragment() { + + private var _binding: FragmentSearchCategoryRestaurantBinding? = null + private val binding get() = _binding!! + + val viewModel: AllSearchViewModel by activityViewModels() + private val searchCategoryRestaurantAdapter = SearchCategoryRestaurantAdapter() + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSearchCategoryRestaurantBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + observeState() + setSpinners() + + viewModel.setSearchKeyword(searchKeyword) + + binding.restaurantRecyclerView.adapter = searchCategoryRestaurantAdapter + binding.restaurantRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + + } + + private fun observeState() { + repeatWhenUiStarted { + viewModel.searchedRestaurantState.collect { + searchCategoryRestaurantAdapter.submitData(it) + } + } + + repeatWhenUiStarted { + viewModel.sortTypeState.collectLatest { + binding.sortSpinner.setMenuTitle(it.text) + } + } + + repeatWhenUiStarted { + viewModel.foodCategoryState.collectLatest { + binding.foodCategorySpinner.setMenuTitle(it.text) + } + } + + repeatWhenUiStarted { + viewModel.drinkPossibilityState.collectLatest { + binding.drinkPossibilitySpinner.setMenuTitle(it.text) + } + } + } + + private fun setSpinners() { + binding.sortSpinner.setMenu( + SortType.getAllText() + ) { + viewModel.setSortType(SortType.values()[it.itemId]) + } + + binding.foodCategorySpinner.setMenu( + FoodCategory.getAllText() + ) { + viewModel.setFoodCategory(FoodCategory.values()[it.itemId]) + } + + binding.drinkPossibilitySpinner.setMenu( + DrinkPossibility.getAllText() + ) { + viewModel.setDrinkPossibility(DrinkPossibility.values()[it.itemId]) + } + } + + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryGroupAdapter.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryGroupAdapter.kt new file mode 100644 index 00000000..e8d5e37a --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryGroupAdapter.kt @@ -0,0 +1,64 @@ +package org.gdsc.presentation.view.allsearch.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import org.gdsc.domain.model.GroupInfo +import org.gdsc.presentation.R +import org.gdsc.presentation.databinding.ItemSearchGroupBinding + +class SearchCategoryGroupAdapter() : + PagingDataAdapter( + DiffCallback + ) { + + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: GroupInfo, newItem: GroupInfo): Boolean { + return oldItem.groupId == newItem.groupId + } + + override fun areContentsTheSame(oldItem: GroupInfo, newItem: GroupInfo): Boolean { + return oldItem == newItem + } + } + + class SearchCategoryGroupViewHolder( + private val binding: ItemSearchGroupBinding, + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GroupInfo) { + binding.run { + Glide.with(itemView.context) + .load(item.groupProfileImageUrl) + .placeholder(R.drawable.base_profile_image) + .into(ivGroupImage) + + tvGroupName.text = item.groupName + tvIntroduction.text = item.groupIntroduce + tvMemberCount.text = item.memberCnt.toString() + tvRestaurantCount.text = item.restaurantCnt.toString() + } + } + } + + override fun onBindViewHolder( + holder: SearchCategoryGroupViewHolder, + position: Int + ) { + val item = getItem(position) + if (item != null) { + holder.bind(item) + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SearchCategoryGroupViewHolder { + val binding = + ItemSearchGroupBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return SearchCategoryGroupViewHolder(binding) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryGroupPreviewAdapter.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryGroupPreviewAdapter.kt new file mode 100644 index 00000000..00e6c93e --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryGroupPreviewAdapter.kt @@ -0,0 +1,69 @@ +package org.gdsc.presentation.view.allsearch.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import org.gdsc.domain.model.GroupInfo +import org.gdsc.presentation.R +import org.gdsc.presentation.databinding.ItemSearchGroupBinding + +class SearchCategoryGroupPreviewAdapter + : ListAdapter( + diffCallback +) { + + companion object { + val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: GroupInfo, + newItem: GroupInfo + ): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame( + oldItem: GroupInfo, + newItem: GroupInfo + ): Boolean { + return oldItem == newItem + } + } + } + + class GroupWithSearchPreviewViewHolder( + private val binding: ItemSearchGroupBinding, + ): RecyclerView.ViewHolder(binding.root) { + fun bind(item: GroupInfo) { + binding.run { + Glide.with(itemView.context) + .load(item.groupProfileImageUrl) + .placeholder(R.drawable.base_profile_image) + .into(ivGroupImage) + + tvGroupName.text = item.groupName + tvIntroduction.text = item.groupIntroduce + tvMemberCount.text = item.memberCnt.toString() + tvRestaurantCount.text = item.restaurantCnt.toString() + } + } + } + + override fun onBindViewHolder(holder: GroupWithSearchPreviewViewHolder, position: Int) { + val item = getItem(position) + if (item != null) { + holder.bind(item) + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): GroupWithSearchPreviewViewHolder { + val binding = ItemSearchGroupBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return GroupWithSearchPreviewViewHolder(binding) + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryPagerAdapter.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryPagerAdapter.kt new file mode 100644 index 00000000..820017ac --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryPagerAdapter.kt @@ -0,0 +1,30 @@ +package org.gdsc.presentation.view.allsearch.adapter + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import org.gdsc.presentation.view.allsearch.SearchCategoryAllFragment +import org.gdsc.presentation.view.allsearch.SearchCategoryGroupFragment +import org.gdsc.presentation.view.allsearch.SearchCategoryRestaurantFragment + +class SearchCategoryPagerAdapter( + fragment: Fragment, + private val keyword: String +) : FragmentStateAdapter(fragment) { + override fun getItemCount() = SEARCH_CATEGORY_PAGER_SIZE + + override fun createFragment(position: Int): Fragment { + return when (position) { + CATEGORY_RESTAURANT -> SearchCategoryRestaurantFragment(keyword) + CATEGORY_GROUP -> SearchCategoryGroupFragment(keyword) + else -> SearchCategoryAllFragment(keyword) + } + } + + companion object { + private const val SEARCH_CATEGORY_PAGER_SIZE = 3 + + const val CATEGORY_ALL = 0 + const val CATEGORY_RESTAURANT = 1 + const val CATEGORY_GROUP = 2 + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryRestaurantAdapter.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryRestaurantAdapter.kt new file mode 100644 index 00000000..90b39ed4 --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryRestaurantAdapter.kt @@ -0,0 +1,79 @@ +package org.gdsc.presentation.view.allsearch.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.IntRange +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import org.gdsc.domain.model.RegisteredRestaurant +import org.gdsc.presentation.R +import org.gdsc.presentation.databinding.ItemSearchRestaurantBinding + +class SearchCategoryRestaurantAdapter + : PagingDataAdapter( + diffCallback +) { + + private var maxItemCount = Int.MAX_VALUE + companion object { + val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: RegisteredRestaurant, + newItem: RegisteredRestaurant + ): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame( + oldItem: RegisteredRestaurant, + newItem: RegisteredRestaurant + ): Boolean { + return oldItem == newItem + } + } + } + + class RestaurantsWithSearchViewHolder( + private val binding: ItemSearchRestaurantBinding, + ): RecyclerView.ViewHolder(binding.root) { + fun bind(item: RegisteredRestaurant) { + binding.run { + Glide.with(itemView.context) + .load(item.userProfileImageUrl) + .placeholder(R.drawable.base_profile_image) + .into(userProfileImage) + + userName.text = item.userNickName + + Glide.with(itemView.context) + .load(item.restaurantImageUrl) + .placeholder(R.drawable.base_profile_image) + .into(restaurantImage) + + restaurantCategory.text = item.category + restaurantName.text = item.name + } + } + } + + override fun onBindViewHolder(holder: RestaurantsWithSearchViewHolder, position: Int) { + val item = getItem(position) + if (item != null) { + holder.bind(item) + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RestaurantsWithSearchViewHolder { + val binding = ItemSearchRestaurantBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return RestaurantsWithSearchViewHolder(binding) + } + + fun setMaxItemCount(@IntRange(from = 1, to = Long.MAX_VALUE) maxItemCount: Int) { + this.maxItemCount = maxItemCount + } +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryRestaurantPreviewAdapter.kt b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryRestaurantPreviewAdapter.kt new file mode 100644 index 00000000..4bf2c7b1 --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/allsearch/adapter/SearchCategoryRestaurantPreviewAdapter.kt @@ -0,0 +1,74 @@ +package org.gdsc.presentation.view.allsearch.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import org.gdsc.domain.model.RegisteredRestaurant +import org.gdsc.presentation.R +import org.gdsc.presentation.databinding.ItemSearchRestaurantBinding + +class SearchCategoryRestaurantPreviewAdapter + : ListAdapter( + diffCallback +) { + + companion object { + val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: RegisteredRestaurant, + newItem: RegisteredRestaurant + ): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame( + oldItem: RegisteredRestaurant, + newItem: RegisteredRestaurant + ): Boolean { + return oldItem == newItem + } + } + } + + class RestaurantsWithSearchPreviewViewHolder( + private val binding: ItemSearchRestaurantBinding, + ): RecyclerView.ViewHolder(binding.root) { + fun bind(item: RegisteredRestaurant) { + binding.run { + Glide.with(itemView.context) + .load(item.userProfileImageUrl) + .placeholder(R.drawable.base_profile_image) + .into(userProfileImage) + + userName.text = item.userNickName + + Glide.with(itemView.context) + .load(item.restaurantImageUrl) + .placeholder(R.drawable.base_profile_image) + .into(restaurantImage) + + restaurantCategory.text = item.category + restaurantName.text = item.name + } + } + } + + override fun onBindViewHolder(holder: RestaurantsWithSearchPreviewViewHolder, position: Int) { + val item = getItem(position) + if (item != null) { + holder.bind(item) + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RestaurantsWithSearchPreviewViewHolder { + val binding = ItemSearchRestaurantBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return RestaurantsWithSearchPreviewViewHolder(binding) + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/custom/JmtSearchEditText.kt b/presentation/src/main/java/org/gdsc/presentation/view/custom/JmtSearchEditText.kt index 7150e7fe..6f921349 100644 --- a/presentation/src/main/java/org/gdsc/presentation/view/custom/JmtSearchEditText.kt +++ b/presentation/src/main/java/org/gdsc/presentation/view/custom/JmtSearchEditText.kt @@ -4,12 +4,24 @@ import android.annotation.SuppressLint import androidx.constraintlayout.widget.ConstraintLayout import android.content.Context import android.util.AttributeSet +import android.util.Log +import android.view.KeyEvent import android.view.LayoutInflater +import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager +import androidx.core.view.isVisible +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.gdsc.presentation.R +import org.gdsc.presentation.base.CancelViewListener +import org.gdsc.presentation.base.SearchViewListener import org.gdsc.presentation.databinding.JmtSearchEditTextBinding import org.gdsc.presentation.utils.fadeIn import org.gdsc.presentation.utils.fadeOut +import kotlin.coroutines.CoroutineContext @SuppressLint("ClickableViewAccessibility") class JmtSearchEditText(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) { @@ -19,6 +31,11 @@ class JmtSearchEditText(context: Context, attrs: AttributeSet) : ConstraintLayou val text get() = binding.searchEditText.text.toString() val editText get() = binding.searchEditText + private var searchViewListener: SearchViewListener? = null + private var cancelViewListener: CancelViewListener? = null + private var afterTextChanged: ((String?) -> Unit)? = null + private var autoSubmit = false + init { context.theme.obtainStyledAttributes( @@ -39,7 +56,10 @@ class JmtSearchEditText(context: Context, attrs: AttributeSet) : ConstraintLayou addView(binding.root) setCancel() + setClear() setAnimation() + setEditorKeyListener() + setTextChangedListener() } @@ -49,6 +69,14 @@ class JmtSearchEditText(context: Context, attrs: AttributeSet) : ConstraintLayou binding.searchEditText.clearFocus() hideKeyboardFromEditText() binding.searchIcon.fadeIn() + cancelViewListener?.onCancel() + } + } + + private fun setClear() { + binding.clearIcon.setOnClickListener { + binding.searchEditText.text?.clear() + searchViewListener?.onSearchClear() } } @@ -67,10 +95,140 @@ class JmtSearchEditText(context: Context, attrs: AttributeSet) : ConstraintLayou } + private fun setEditorKeyListener() { + binding.searchEditText.setOnEditorActionListener { v, actionID, event -> + + if (actionID == EditorInfo.IME_ACTION_SEARCH) { + clearFocus() + hideKeyboardFromEditText() + searchViewListener?.onSubmitText(v.text.toString()) + true + } else { + false + } + } + + binding.searchEditText.onKey { _, keyCode, _ -> + + if (keyCode == KeyEvent.KEYCODE_ENTER) { + clearFocus() + hideKeyboardFromEditText() + searchViewListener?.onSubmitText(binding.searchEditText.text.toString()) + } + } + } + + + private fun setTextChangedListener() { + binding.searchEditText.textChangedListener { + onTextChanged { charSequence, _, _, _ -> + binding.clearIcon.isVisible = !charSequence.isNullOrEmpty() + searchViewListener?.onChangeText(charSequence ?: "") + if (autoSubmit) { + searchViewListener?.onSubmitText(binding.searchEditText.text.toString()) + autoSubmit = false + } + } + + afterTextChanged { + afterTextChanged?.invoke(it.toString()) + } + + } + } private fun hideKeyboardFromEditText() { val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(binding.searchEditText.windowToken, 0) } + fun setSearchViewListener(searchViewListener: SearchViewListener) { + this.searchViewListener = searchViewListener + } + + fun setCancelViewListener(cancelViewListener: CancelViewListener) { + this.cancelViewListener = cancelViewListener + } + + fun setSearchText(text: String) { + binding.searchEditText.setText(text) + } + + fun setAfterTextChanged(action: (String?) -> Unit) { + afterTextChanged = action + } + + fun android.view.View.onKey( + context: CoroutineContext = Dispatchers.Main, + returnValue: Boolean = false, + handler: suspend CoroutineScope.(v: android.view.View, keyCode: Int, event: android.view.KeyEvent?) -> Unit + ) { + setOnKeyListener { v, keyCode, event -> + GlobalScope.launch(context, CoroutineStart.DEFAULT) { + handler(v, keyCode, event) + } + returnValue + } + } + + fun android.widget.TextView.textChangedListener( + context: CoroutineContext = Dispatchers.Main, + init: __TextWatcher.() -> Unit + ) { + val listener = __TextWatcher(context) + listener.init() + addTextChangedListener(listener) + } } + +class __TextWatcher(private val context: CoroutineContext) : android.text.TextWatcher { + + private var _beforeTextChanged: (suspend CoroutineScope.(CharSequence?, Int, Int, Int) -> Unit)? = null + + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + val handler = _beforeTextChanged ?: return + GlobalScope.launch(context) { + handler(s, start, count, after) + } + } + + fun beforeTextChanged( + listener: suspend CoroutineScope.(CharSequence?, Int, Int, Int) -> Unit + ) { + _beforeTextChanged = listener + } + + private var _onTextChanged: (suspend CoroutineScope.(CharSequence?, Int, Int, Int) -> Unit)? = null + + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + val handler = _onTextChanged ?: return + GlobalScope.launch(context) { + handler(s, start, before, count) + } + } + + fun onTextChanged( + listener: suspend CoroutineScope.(CharSequence?, Int, Int, Int) -> Unit + ) { + _onTextChanged = listener + } + + private var _afterTextChanged: (suspend CoroutineScope.(android.text.Editable?) -> Unit)? = null + + + override fun afterTextChanged(s: android.text.Editable?) { + val handler = _afterTextChanged ?: return + GlobalScope.launch(context) { + handler(s) + } + } + + fun afterTextChanged( + listener: suspend CoroutineScope.(android.text.Editable?) -> Unit + ) { + _afterTextChanged = listener + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/mypage/viewmodel/MyPageViewModel.kt b/presentation/src/main/java/org/gdsc/presentation/view/mypage/viewmodel/MyPageViewModel.kt index f9286182..47668750 100644 --- a/presentation/src/main/java/org/gdsc/presentation/view/mypage/viewmodel/MyPageViewModel.kt +++ b/presentation/src/main/java/org/gdsc/presentation/view/mypage/viewmodel/MyPageViewModel.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -24,11 +23,9 @@ import org.gdsc.domain.DrinkPossibility import org.gdsc.domain.Empty import org.gdsc.domain.FoodCategory import org.gdsc.domain.SortType -import org.gdsc.domain.model.Filter import org.gdsc.domain.model.Location import org.gdsc.domain.model.PagingResult import org.gdsc.domain.model.RegisteredRestaurant -import org.gdsc.domain.model.request.RestaurantSearchMapRequest import org.gdsc.domain.model.response.NicknameResponse import org.gdsc.domain.usecase.CheckDuplicatedNicknameUseCase import org.gdsc.domain.usecase.GetRegisteredRestaurantUseCase diff --git a/presentation/src/main/res/drawable/ic_x_clear.xml b/presentation/src/main/res/drawable/ic_x_clear.xml new file mode 100644 index 00000000..e1dd73ef --- /dev/null +++ b/presentation/src/main/res/drawable/ic_x_clear.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/presentation/src/main/res/layout/fragment_all_search.xml b/presentation/src/main/res/layout/fragment_all_search.xml new file mode 100644 index 00000000..4e73cb01 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_all_search.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_all_search_container.xml b/presentation/src/main/res/layout/fragment_all_search_container.xml new file mode 100644 index 00000000..8ffde272 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_all_search_container.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_search_category_all.xml b/presentation/src/main/res/layout/fragment_search_category_all.xml new file mode 100644 index 00000000..99ac8c7d --- /dev/null +++ b/presentation/src/main/res/layout/fragment_search_category_all.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_search_category_group.xml b/presentation/src/main/res/layout/fragment_search_category_group.xml new file mode 100644 index 00000000..f5cb348d --- /dev/null +++ b/presentation/src/main/res/layout/fragment_search_category_group.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_search_category_restaurant.xml b/presentation/src/main/res/layout/fragment_search_category_restaurant.xml new file mode 100644 index 00000000..24e3c9a7 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_search_category_restaurant.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_search_group.xml b/presentation/src/main/res/layout/item_search_group.xml new file mode 100644 index 00000000..4ace9436 --- /dev/null +++ b/presentation/src/main/res/layout/item_search_group.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/item_search_restaurant.xml b/presentation/src/main/res/layout/item_search_restaurant.xml new file mode 100644 index 00000000..bd66f033 --- /dev/null +++ b/presentation/src/main/res/layout/item_search_restaurant.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/jmt_search_edit_text.xml b/presentation/src/main/res/layout/jmt_search_edit_text.xml index de18161b..ec6239ae 100644 --- a/presentation/src/main/res/layout/jmt_search_edit_text.xml +++ b/presentation/src/main/res/layout/jmt_search_edit_text.xml @@ -19,7 +19,7 @@ android:id="@+id/search_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="12dp" + android:layout_marginStart="@dimen/half_element_spacing" android:layout_marginVertical="19dp" android:src="@drawable/search_text_icon" app:layout_constraintStart_toStartOf="parent" @@ -39,13 +39,23 @@ android:textColorHint="@color/grey200" android:textColor="@color/grey900" android:textCursorDrawable="@null" + android:imeOptions="actionSearch" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@+id/clear_icon" app:layout_constraintStart_toEndOf="@id/search_icon" app:layout_constraintTop_toTopOf="parent" android:inputType="text" /> + + android:name="org.gdsc.presentation.view.allsearch.AllSearchFragment" + android:label="all_search_fragment" + tools:layout="@layout/fragment_all_search" > + android:id="@+id/action_all_search_fragment_to_all_search_container_fragment" + app:destination="@+id/all_search_container_fragment"/> + + + 1dp 2dp 8dp + 12dp 20dp 10dp diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index e47b29bc..0fec161b 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -46,6 +46,11 @@ 선택 선택하기 등록하기 + + + 전체 + 맛집 + 그룹 내 위치에서 %s 위치 정보를 가져올 수 없습니다.