From 29b626ff4555d4e8ee50b25767ec69f244f1c9d7 Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Tue, 1 Oct 2024 16:54:14 +0900 Subject: [PATCH 01/11] feat: add SearchFestivalApi & SearchFestivalRepository --- .../kr/ksw/visitkorea/data/di/DataModule.kt | 13 ++++++++ .../data/remote/api/SearchFestivalApi.kt | 19 ++++++++++++ .../data/remote/dto/SearchFestivalDTO.kt | 31 +++++++++++++++++++ .../repository/SearchFestivalRepository.kt | 14 +++++++++ .../SearchFestivalRepositoryImpl.kt | 28 +++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 app/src/main/java/kr/ksw/visitkorea/data/remote/api/SearchFestivalApi.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/data/remote/dto/SearchFestivalDTO.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/data/repository/SearchFestivalRepository.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/data/repository/SearchFestivalRepositoryImpl.kt diff --git a/app/src/main/java/kr/ksw/visitkorea/data/di/DataModule.kt b/app/src/main/java/kr/ksw/visitkorea/data/di/DataModule.kt index 39265a8..3160761 100644 --- a/app/src/main/java/kr/ksw/visitkorea/data/di/DataModule.kt +++ b/app/src/main/java/kr/ksw/visitkorea/data/di/DataModule.kt @@ -10,10 +10,13 @@ import kr.ksw.visitkorea.data.local.databases.AreaCodeDatabase import kr.ksw.visitkorea.data.remote.api.AreaCodeApi import kr.ksw.visitkorea.data.remote.api.LocationBasedListApi import kr.ksw.visitkorea.data.remote.api.RetrofitInterceptor +import kr.ksw.visitkorea.data.remote.api.SearchFestivalApi import kr.ksw.visitkorea.data.repository.AreaCodeRepository import kr.ksw.visitkorea.data.repository.AreaCodeRepositoryImpl import kr.ksw.visitkorea.data.repository.LocationBasedListRepository import kr.ksw.visitkorea.data.repository.LocationBasedListRepositoryImpl +import kr.ksw.visitkorea.data.repository.SearchFestivalRepository +import kr.ksw.visitkorea.data.repository.SearchFestivalRepositoryImpl import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -72,4 +75,14 @@ object DataModule { fun provideLocationBasedListRepository(locationBasedListApi: LocationBasedListApi): LocationBasedListRepository { return LocationBasedListRepositoryImpl(locationBasedListApi) } + + @Provides + @Singleton + fun provideSearchFestivalApi(retrofit: Retrofit): SearchFestivalApi = retrofit.create(SearchFestivalApi::class.java) + + @Provides + @Singleton + fun provideSearchFestivalRepository(searchFestivalApi: SearchFestivalApi): SearchFestivalRepository { + return SearchFestivalRepositoryImpl(searchFestivalApi) + } } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/data/remote/api/SearchFestivalApi.kt b/app/src/main/java/kr/ksw/visitkorea/data/remote/api/SearchFestivalApi.kt new file mode 100644 index 0000000..7f96592 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/data/remote/api/SearchFestivalApi.kt @@ -0,0 +1,19 @@ +package kr.ksw.visitkorea.data.remote.api + +import kr.ksw.visitkorea.data.remote.dto.SearchFestivalDTO +import kr.ksw.visitkorea.data.remote.model.ApiResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface SearchFestivalApi { + @GET("searchFestival1") + suspend fun searchFestival( + @Query("arrange") arrange: String = "S", + @Query("numOfRows") numOfRows: Int, + @Query("pageNo") pageNo: Int, + @Query("eventStartDate") eventStartDate: String, + @Query("eventEndDate") eventEndDate: String, + @Query("areaCode") areaCode: String? = null, + @Query("sigunguCode") sigunguCode: String? = null + ): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/data/remote/dto/SearchFestivalDTO.kt b/app/src/main/java/kr/ksw/visitkorea/data/remote/dto/SearchFestivalDTO.kt new file mode 100644 index 0000000..5a97d95 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/data/remote/dto/SearchFestivalDTO.kt @@ -0,0 +1,31 @@ +package kr.ksw.visitkorea.data.remote.dto + +import com.google.gson.annotations.SerializedName + +data class SearchFestivalDTO( + @SerializedName("addr1") + val address: String, + @SerializedName("areacode") + val areaCode: String, + @SerializedName("sigungucode") + val sigunguCode: String, + @SerializedName("contentid") + val contentId: String, + @SerializedName("contenttypeid") + val contentTypeId: String, + @SerializedName("eventstartdate") + val eventStartDate: String, + @SerializedName("eventenddate") + val eventEndDate: String, + @SerializedName("firstimage") + val firstImage: String, + @SerializedName("firstimage2") + val firstImage2: String, + @SerializedName("mapx") + val mapX: String, + @SerializedName("mapy") + val mapY: String, + val cat3: String, + val tel: String, + val title: String +) \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/data/repository/SearchFestivalRepository.kt b/app/src/main/java/kr/ksw/visitkorea/data/repository/SearchFestivalRepository.kt new file mode 100644 index 0000000..0c8a5f1 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/data/repository/SearchFestivalRepository.kt @@ -0,0 +1,14 @@ +package kr.ksw.visitkorea.data.repository + +import kr.ksw.visitkorea.data.remote.dto.SearchFestivalDTO + +interface SearchFestivalRepository { + suspend operator fun invoke( + numOfRows: Int, + pageNo: Int, + eventStartDate: String, + eventEndDate: String, + areaCode: String? = null, + sigunguCode: String? = null + ): Result> +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/data/repository/SearchFestivalRepositoryImpl.kt b/app/src/main/java/kr/ksw/visitkorea/data/repository/SearchFestivalRepositoryImpl.kt new file mode 100644 index 0000000..0d24ca3 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/data/repository/SearchFestivalRepositoryImpl.kt @@ -0,0 +1,28 @@ +package kr.ksw.visitkorea.data.repository + +import kr.ksw.visitkorea.data.mapper.toItems +import kr.ksw.visitkorea.data.remote.api.SearchFestivalApi +import kr.ksw.visitkorea.data.remote.dto.SearchFestivalDTO +import javax.inject.Inject + +class SearchFestivalRepositoryImpl @Inject constructor( + private val searchFestivalApi: SearchFestivalApi +): SearchFestivalRepository { + override suspend fun invoke( + numOfRows: Int, + pageNo: Int, + eventStartDate: String, + eventEndDate: String, + areaCode: String?, + sigunguCode: String? + ): Result> = runCatching { + searchFestivalApi.searchFestival( + numOfRows = numOfRows, + pageNo = pageNo, + eventStartDate = eventStartDate, + eventEndDate = eventEndDate, + areaCode = areaCode, + sigunguCode = sigunguCode + ).toItems() + } +} \ No newline at end of file From 2840c21eaa59b92507e76877ee40ad3538e45db9 Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Tue, 1 Oct 2024 17:09:39 +0900 Subject: [PATCH 02/11] feat: add festival paging source & GetFestivalListUseCase --- .../source/SearchFestivalPagingSource.kt | 42 ++++++++++++++++ .../visitkorea/domain/di/FestivalModule.kt | 15 ++++++ .../festival/GetFestivalListUseCase.kt | 15 ++++++ .../festival/GetFestivalListUseCaseImpl.kt | 48 +++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/domain/di/FestivalModule.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCase.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCaseImpl.kt diff --git a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt new file mode 100644 index 0000000..cafa1a8 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt @@ -0,0 +1,42 @@ +package kr.ksw.visitkorea.data.paging.source + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import kr.ksw.visitkorea.data.mapper.toItems +import kr.ksw.visitkorea.data.remote.api.SearchFestivalApi +import kr.ksw.visitkorea.data.remote.dto.SearchFestivalDTO + +class SearchFestivalPagingSource( + private val searchFestivalApi: SearchFestivalApi, + private val eventStartDate: String, + private val eventEndDate: String, + private val areaCode: String?, + private val sigunguCode: String? +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchor -> + val anchorPage = state.closestPageToPosition(anchor) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 1 + val loadSize = params.loadSize + val response = searchFestivalApi.searchFestival( + numOfRows = loadSize, + pageNo = page, + eventStartDate = eventStartDate, + eventEndDate = eventEndDate, + areaCode = areaCode, + sigunguCode = sigunguCode + ) + val data = response.toItems() + return LoadResult.Page( + data = data, + prevKey = if(page == 1) null else page - 1, + nextKey = if(data.size == loadSize) page + 1 else null + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/di/FestivalModule.kt b/app/src/main/java/kr/ksw/visitkorea/domain/di/FestivalModule.kt new file mode 100644 index 0000000..2a630fc --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/domain/di/FestivalModule.kt @@ -0,0 +1,15 @@ +package kr.ksw.visitkorea.domain.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import kr.ksw.visitkorea.domain.usecase.festival.GetFestivalListUseCase +import kr.ksw.visitkorea.domain.usecase.festival.GetFestivalListUseCaseImpl + +@Module +@InstallIn(ActivityRetainedComponent::class) +abstract class FestivalModule { + @Binds + abstract fun bindGetFestivalListUseCase(getFestivalListUseCase: GetFestivalListUseCaseImpl): GetFestivalListUseCase +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCase.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCase.kt new file mode 100644 index 0000000..f527c27 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCase.kt @@ -0,0 +1,15 @@ +package kr.ksw.visitkorea.domain.usecase.festival + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kr.ksw.visitkorea.data.remote.dto.SearchFestivalDTO + +interface GetFestivalListUseCase { + suspend operator fun invoke( + forceFetch: Boolean, + eventStartDate: String, + eventEndDate: String, + areaCode: String?, + sigunguCode: String? + ) : Result>> +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCaseImpl.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCaseImpl.kt new file mode 100644 index 0000000..00ca36b --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCaseImpl.kt @@ -0,0 +1,48 @@ +package kr.ksw.visitkorea.domain.usecase.festival + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import kotlinx.coroutines.flow.Flow +import kr.ksw.visitkorea.data.paging.source.SearchFestivalPagingSource +import kr.ksw.visitkorea.data.remote.api.SearchFestivalApi +import kr.ksw.visitkorea.data.remote.dto.SearchFestivalDTO +import javax.inject.Inject + +class GetFestivalListUseCaseImpl @Inject constructor( + private val searchFestivalApi: SearchFestivalApi +) : GetFestivalListUseCase { + private var pagingSource: PagingSource? = null + + override suspend fun invoke( + forceFetch: Boolean, + eventStartDate: String, + eventEndDate: String, + areaCode: String?, + sigunguCode: String? + ): Result>> = runCatching { + if(forceFetch && + pagingSource != null && + pagingSource?.invalid != true) { + pagingSource?.invalidate() + } + Pager( + config = PagingConfig( + pageSize = 10, + initialLoadSize = 10 + ), + pagingSourceFactory = { + SearchFestivalPagingSource( + searchFestivalApi, + eventStartDate, + eventEndDate, + areaCode, + sigunguCode + ).also { + pagingSource = it + } + } + ).flow + } +} \ No newline at end of file From 93adf39e9c441eb28c616040c96185a2ba6cb9fd Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Wed, 2 Oct 2024 08:49:37 +0900 Subject: [PATCH 03/11] refactor: move Cards to component --- .../presentation/home/{screen => component}/CultureCard.kt | 4 +--- .../home/{screen => component}/RestaurantCard.kt | 2 +- .../home/{screen => component}/TouristSpotCard.kt | 2 +- .../kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt | 3 +++ .../ksw/visitkorea/presentation/hotel/screen/HotelScreen.kt | 5 +---- 5 files changed, 7 insertions(+), 9 deletions(-) rename app/src/main/java/kr/ksw/visitkorea/presentation/home/{screen => component}/CultureCard.kt (96%) rename app/src/main/java/kr/ksw/visitkorea/presentation/home/{screen => component}/RestaurantCard.kt (98%) rename app/src/main/java/kr/ksw/visitkorea/presentation/home/{screen => component}/TouristSpotCard.kt (98%) diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/CultureCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/CultureCard.kt similarity index 96% rename from app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/CultureCard.kt rename to app/src/main/java/kr/ksw/visitkorea/presentation/home/component/CultureCard.kt index cc454d4..f327d49 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/CultureCard.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/CultureCard.kt @@ -1,4 +1,4 @@ -package kr.ksw.visitkorea.presentation.home.screen +package kr.ksw.visitkorea.presentation.home.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -7,10 +7,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.LocationOn diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/RestaurantCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt similarity index 98% rename from app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/RestaurantCard.kt rename to app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt index d701279..e9bd8c2 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/RestaurantCard.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt @@ -1,4 +1,4 @@ -package kr.ksw.visitkorea.presentation.home.screen +package kr.ksw.visitkorea.presentation.home.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/TouristSpotCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/TouristSpotCard.kt similarity index 98% rename from app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/TouristSpotCard.kt rename to app/src/main/java/kr/ksw/visitkorea/presentation/home/component/TouristSpotCard.kt index 81edc16..ca34a3f 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/TouristSpotCard.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/TouristSpotCard.kt @@ -1,4 +1,4 @@ -package kr.ksw.visitkorea.presentation.home.screen +package kr.ksw.visitkorea.presentation.home.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt index 79f30ea..95a5652 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt @@ -38,6 +38,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import coil.request.ImageRequest import coil.size.Size +import kr.ksw.visitkorea.presentation.home.component.CultureCard +import kr.ksw.visitkorea.presentation.home.component.RestaurantCard +import kr.ksw.visitkorea.presentation.home.component.TouristSpotCard import kr.ksw.visitkorea.presentation.home.viewmodel.HomeState import kr.ksw.visitkorea.presentation.home.viewmodel.HomeViewModel import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/hotel/screen/HotelScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/hotel/screen/HotelScreen.kt index de31ee1..1510736 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/hotel/screen/HotelScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/hotel/screen/HotelScreen.kt @@ -1,6 +1,5 @@ package kr.ksw.visitkorea.presentation.hotel.screen -import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -9,9 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,7 +23,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import kr.ksw.visitkorea.domain.usecase.model.CommonCardModel -import kr.ksw.visitkorea.presentation.home.screen.CultureCard +import kr.ksw.visitkorea.presentation.home.component.CultureCard import kr.ksw.visitkorea.presentation.hotel.viewmodel.HotelViewModel import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme From 2d99764de74d4ad2b32abcb9f32571969ac43059 Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Wed, 2 Oct 2024 08:50:01 +0900 Subject: [PATCH 04/11] feat: add festival screen --- .../mapper/ToPresentationModelMapper.kt | 12 ++ .../domain/usecase/model/Festival.kt | 10 ++ .../domain/usecase/util/StringConvertUtil.kt | 5 + .../festival/component/FestivalCard.kt | 169 ++++++++++++++++++ .../festival/screen/FestivalScreen.kt | 114 ++++++++++++ .../festival/viewmodel/FestivalState.kt | 12 ++ .../festival/viewmodel/FestivalViewModel.kt | 53 ++++++ .../hotel/viewmodel/HotelViewModel.kt | 1 - .../presentation/main/MainNavHost.kt | 8 +- 9 files changed, 377 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/kr/ksw/visitkorea/domain/usecase/model/Festival.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/festival/component/FestivalCard.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/festival/screen/FestivalScreen.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/festival/viewmodel/FestivalState.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/festival/viewmodel/FestivalViewModel.kt diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/mapper/ToPresentationModelMapper.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/mapper/ToPresentationModelMapper.kt index 9456de8..9ed12b5 100644 --- a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/mapper/ToPresentationModelMapper.kt +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/mapper/ToPresentationModelMapper.kt @@ -1,9 +1,12 @@ package kr.ksw.visitkorea.domain.usecase.mapper import kr.ksw.visitkorea.data.remote.dto.LocationBasedDTO +import kr.ksw.visitkorea.data.remote.dto.SearchFestivalDTO import kr.ksw.visitkorea.domain.usecase.model.CommonCardModel +import kr.ksw.visitkorea.domain.usecase.model.Festival import kr.ksw.visitkorea.domain.usecase.model.Restaurant import kr.ksw.visitkorea.domain.usecase.model.TouristSpot +import kr.ksw.visitkorea.domain.usecase.util.toDateString import kr.ksw.visitkorea.domain.usecase.util.toDistForUi import kr.ksw.visitkorea.domain.usecase.util.toImageUrl @@ -23,6 +26,15 @@ fun LocationBasedDTO.toCommonCardModel(): CommonCardModel = CommonCardModel( contentId ) +fun SearchFestivalDTO.toFestival(): Festival = Festival( + address, + firstImage.toImageUrl(), + title, + contentId, + eventStartDate.toDateString(), + eventEndDate.toDateString() +) + val restaurantMap = mapOf( "A05020100" to "한식", "A05020200" to "서양식", diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/model/Festival.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/model/Festival.kt new file mode 100644 index 0000000..6723bd1 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/model/Festival.kt @@ -0,0 +1,10 @@ +package kr.ksw.visitkorea.domain.usecase.model + +data class Festival( + val address: String = "", + val firstImage: String = "", + val title: String = "", + val contentId: String = "", + val eventStartDate: String = "", + val eventEndDate: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/util/StringConvertUtil.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/util/StringConvertUtil.kt index 2e1c3c8..e532e27 100644 --- a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/util/StringConvertUtil.kt +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/util/StringConvertUtil.kt @@ -9,4 +9,9 @@ fun String.toDistForUi(): String { } else { "${meters[0]}m" } +} + +fun String.toDateString(): String { + val sb = StringBuilder(this.substring(4)) + return sb.insert(2, ".").toString() } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/festival/component/FestivalCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/component/FestivalCard.kt new file mode 100644 index 0000000..c9c1af6 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/component/FestivalCard.kt @@ -0,0 +1,169 @@ +package kr.ksw.visitkorea.presentation.festival.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.LocationOn +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import coil.size.Size +import kr.ksw.visitkorea.domain.usecase.model.Festival +import kr.ksw.visitkorea.presentation.component.SingleLineText +import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme + +@Composable +fun FestivalCard( + festival: Festival +) { + Card( + shape = RoundedCornerShape(24.dp), + elevation = CardDefaults.elevatedCardElevation( + defaultElevation = 6.dp + ) + ) { + Box( + modifier = Modifier + .background(Color.White) + .padding( + top = 8.dp, + start = 8.dp, + end = 8.dp + ) + ) { + AsyncImage( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(2f) + .clip(RoundedCornerShape(24.dp)) + .background(color = Color.LightGray), + model = ImageRequest + .Builder(LocalContext.current) + .data(festival.firstImage) + .size(Size.ORIGINAL) + .build(), + contentDescription = "Event Image", + contentScale = ContentScale.Crop, + ) + Column( + modifier = Modifier + .border( + 2.dp, + Color.DarkGray, + RoundedCornerShape( + topStart = 24.dp, + bottomEnd = 24.dp + ) + ) + .background( + Color.Black.copy(alpha = 0.5f), + RoundedCornerShape( + topStart = 24.dp, + bottomEnd = 24.dp + ) + ) + .padding( + vertical = 12.dp, + horizontal = 10.dp + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = festival.eventStartDate, + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + color = Color.White, + letterSpacing = (-0.6).sp + ) + Text( + text = "~", + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + color = Color.White, + ) + Text( + text = festival.eventEndDate, + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + color = Color.White, + letterSpacing = (-0.6).sp + ) + } + SingleLineText( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 10.dp) + .align(Alignment.BottomEnd), + text = festival.title, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = Color.White + ) + } + Row( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding( + top = 8.dp, + start = 10.dp, + end = 10.dp, + bottom = 10.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .size(20.dp), + imageVector = Icons.Outlined.LocationOn, + contentDescription = null, + ) + SingleLineText( + text = festival.address, + fontSize = 16.sp, + ) + } + } +} + +@Composable +@Preview(showBackground = true) +fun FestivalCardPreview() { + VisitKoreaTheme { + Surface { + FestivalCard(Festival( + "경상북도 칠곡군 동명면 남원리", + "", + "가산산성 문화유산 야행", + "1111", + "10.11", + "10.30" + )) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/festival/screen/FestivalScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/screen/FestivalScreen.kt new file mode 100644 index 0000000..b61e082 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/screen/FestivalScreen.kt @@ -0,0 +1,114 @@ +package kr.ksw.visitkorea.presentation.festival.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.LocationOn +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import kr.ksw.visitkorea.domain.usecase.model.Festival +import kr.ksw.visitkorea.presentation.festival.component.FestivalCard +import kr.ksw.visitkorea.presentation.festival.viewmodel.FestivalViewModel +import kr.ksw.visitkorea.presentation.home.component.CultureCard +import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme + +@Composable +fun FestivalScreen( + viewModel: FestivalViewModel = hiltViewModel() +) { + val hotelState by viewModel.festivalState.collectAsState() + val lazyItem = hotelState.festivalModelFlow.collectAsLazyPagingItems() + FestivalScreen( + lazyItem + ) +} + +@Composable +fun FestivalScreen( + festivals: LazyPagingItems, +) { + Surface( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .padding( + top = 20.dp, + start = 16.dp, + end = 16.dp, + bottom = 10.dp + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "축제", + fontSize = 32.sp, + fontWeight = FontWeight.SemiBold + ) + Icon( + Icons.Outlined.LocationOn, + modifier = Modifier + .size(32.dp), + contentDescription = "Location Filter Icon" + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "진행중인 축제를 찾아보세요!", + fontSize = 22.sp, + ) + Spacer(modifier = Modifier.height(16.dp)) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items( + count = festivals.itemCount, + key = { index -> + festivals[index]?.contentId?.toInt() ?: index + } + ) { index -> + val festival = festivals[index] + festival?.run { + val model = this + FestivalCard(model) + } + } + } + } + } +} + +@Composable +@Preview(showBackground = true) +fun FestivalPreview() { + VisitKoreaTheme { + FestivalScreen() + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/festival/viewmodel/FestivalState.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/viewmodel/FestivalState.kt new file mode 100644 index 0000000..39e0fd9 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/viewmodel/FestivalState.kt @@ -0,0 +1,12 @@ +package kr.ksw.visitkorea.presentation.festival.viewmodel + +import androidx.compose.runtime.Immutable +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kr.ksw.visitkorea.domain.usecase.model.Festival + +@Immutable +data class FestivalState( + val festivalModelFlow: Flow> = emptyFlow() +) \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/festival/viewmodel/FestivalViewModel.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/viewmodel/FestivalViewModel.kt new file mode 100644 index 0000000..2c6665b --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/viewmodel/FestivalViewModel.kt @@ -0,0 +1,53 @@ +package kr.ksw.visitkorea.presentation.festival.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import androidx.paging.map +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kr.ksw.visitkorea.domain.usecase.festival.GetFestivalListUseCase +import kr.ksw.visitkorea.domain.usecase.mapper.toFestival +import javax.inject.Inject + +@HiltViewModel +class FestivalViewModel @Inject constructor( + private val getFestivalListUseCase: GetFestivalListUseCase +) : ViewModel() { + private val _festivalState = MutableStateFlow(FestivalState()) + val festivalState: StateFlow + get() = _festivalState.asStateFlow() + + init { + getFestivalList() + } + + private fun getFestivalList(forceFetch: Boolean = false) { + viewModelScope.launch { + val hotelListFlow = getFestivalListUseCase( + forceFetch, + "20241007", + "20241007", + null, + null + ).getOrNull() + if(hotelListFlow != null) { + val festivalModelFlow = hotelListFlow.map { pagingData -> + pagingData.map { + it.toFestival() + } + }.cachedIn(viewModelScope) + _festivalState.update { + it.copy( + festivalModelFlow = festivalModelFlow + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/hotel/viewmodel/HotelViewModel.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/hotel/viewmodel/HotelViewModel.kt index 7b93591..499bace 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/hotel/viewmodel/HotelViewModel.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/hotel/viewmodel/HotelViewModel.kt @@ -40,7 +40,6 @@ class HotelViewModel @Inject constructor( it.toCommonCardModel() } }.cachedIn(viewModelScope) - _hotelState.update { it.copy( hotelCardModelFlow = hotelCardModelFlow diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/main/MainNavHost.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/main/MainNavHost.kt index 915dfcf..f795f40 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/main/MainNavHost.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/main/MainNavHost.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import kr.ksw.visitkorea.presentation.festival.screen.FestivalScreen import kr.ksw.visitkorea.presentation.home.screen.HomeScreen import kr.ksw.visitkorea.presentation.hotel.screen.HotelScreen import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme @@ -32,12 +33,7 @@ fun MainNavHost( HotelScreen() } composable(route = MainRoute.EVENT.route) { - SampleScreen { - Text( - text = "EVENT", - fontSize = 32.sp - ) - } + FestivalScreen() } composable(route = MainRoute.SEARCH.route) { SampleScreen { From 8d136da11245020bf106dc142809ff60e0f7ca49 Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Wed, 2 Oct 2024 16:56:25 +0900 Subject: [PATCH 05/11] feat: add GetMoreListUseCase & add MoreButton to HomeScreen --- .../kr/ksw/visitkorea/domain/di/MoreModule.kt | 17 +++ .../mapper/ToPresentationModelMapper.kt | 10 ++ .../domain/usecase/model/MoreCardModel.kt | 10 ++ .../domain/usecase/more/GetMoreListUseCase.kt | 14 +++ .../usecase/more/GetMoreListUseCaseImpl.kt | 46 +++++++ .../presentation/home/component/MoreButton.kt | 40 ++++++ .../presentation/home/screen/HomeScreen.kt | 119 +++++++++++++----- .../home/viewmodel/HomeUiEffect.kt | 5 + .../home/viewmodel/HomeViewModel.kt | 13 ++ 9 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/kr/ksw/visitkorea/domain/di/MoreModule.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/domain/usecase/model/MoreCardModel.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCase.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCaseImpl.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/home/component/MoreButton.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeUiEffect.kt diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/di/MoreModule.kt b/app/src/main/java/kr/ksw/visitkorea/domain/di/MoreModule.kt new file mode 100644 index 0000000..8701c3f --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/domain/di/MoreModule.kt @@ -0,0 +1,17 @@ +package kr.ksw.visitkorea.domain.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import kr.ksw.visitkorea.domain.usecase.more.GetMoreListUseCase +import kr.ksw.visitkorea.domain.usecase.more.GetMoreListUseCaseImpl + +@Module +@InstallIn(ActivityRetainedComponent::class) +abstract class MoreModule { + @Binds + abstract fun bindGetMoreListUseCase( + getMoreListUseCase: GetMoreListUseCaseImpl + ): GetMoreListUseCase +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/mapper/ToPresentationModelMapper.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/mapper/ToPresentationModelMapper.kt index 9ed12b5..dfb65b0 100644 --- a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/mapper/ToPresentationModelMapper.kt +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/mapper/ToPresentationModelMapper.kt @@ -4,6 +4,7 @@ import kr.ksw.visitkorea.data.remote.dto.LocationBasedDTO import kr.ksw.visitkorea.data.remote.dto.SearchFestivalDTO import kr.ksw.visitkorea.domain.usecase.model.CommonCardModel import kr.ksw.visitkorea.domain.usecase.model.Festival +import kr.ksw.visitkorea.domain.usecase.model.MoreCardModel import kr.ksw.visitkorea.domain.usecase.model.Restaurant import kr.ksw.visitkorea.domain.usecase.model.TouristSpot import kr.ksw.visitkorea.domain.usecase.util.toDateString @@ -51,4 +52,13 @@ fun LocationBasedDTO.toRestaurantModel(): Restaurant = Restaurant( firstImage.toImageUrl(), title, restaurantMap[cat3] ?: "" +) + +fun LocationBasedDTO.toMoreCardModel(): MoreCardModel = MoreCardModel( + address, + firstImage.toImageUrl(), + title, + dist.toDistForUi(), + contentId, + restaurantMap[cat3] ) \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/model/MoreCardModel.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/model/MoreCardModel.kt new file mode 100644 index 0000000..0257887 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/model/MoreCardModel.kt @@ -0,0 +1,10 @@ +package kr.ksw.visitkorea.domain.usecase.model + +data class MoreCardModel( + val address: String = "", + val firstImage: String = "", + val title: String = "", + val dist: String = "", + val contentId: String = "", + val category: String?, +) \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCase.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCase.kt new file mode 100644 index 0000000..2ca9369 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCase.kt @@ -0,0 +1,14 @@ +package kr.ksw.visitkorea.domain.usecase.more + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kr.ksw.visitkorea.data.remote.dto.LocationBasedDTO + +interface GetMoreListUseCase { + suspend operator fun invoke( + forceFetch: Boolean, + mapX: String, + mapY: String, + contentTypeId: String + ) : Result>> +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCaseImpl.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCaseImpl.kt new file mode 100644 index 0000000..a587a76 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCaseImpl.kt @@ -0,0 +1,46 @@ +package kr.ksw.visitkorea.domain.usecase.more + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import kotlinx.coroutines.flow.Flow +import kr.ksw.visitkorea.data.paging.source.LocationBasedPagingSource +import kr.ksw.visitkorea.data.remote.api.LocationBasedListApi +import kr.ksw.visitkorea.data.remote.dto.LocationBasedDTO +import javax.inject.Inject + +class GetMoreListUseCaseImpl @Inject constructor( + private val locationBasedListApi: LocationBasedListApi +): GetMoreListUseCase { + private var pagingSource: PagingSource? = null + + override suspend fun invoke( + forceFetch: Boolean, + mapX: String, + mapY: String, + contentTypeId: String + ): Result>> = runCatching { + if(forceFetch && + pagingSource != null && + pagingSource?.invalid != true) { + pagingSource?.invalidate() + } + Pager( + config = PagingConfig( + pageSize = 10, + initialLoadSize = 10 + ), + pagingSourceFactory = { + LocationBasedPagingSource( + locationBasedListApi, + contentTypeId, + mapX, + mapY + ).also { + pagingSource = it + } + } + ).flow + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/MoreButton.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/MoreButton.kt new file mode 100644 index 0000000..03b168b --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/MoreButton.kt @@ -0,0 +1,40 @@ +package kr.ksw.visitkorea.presentation.home.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +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.unit.dp +import androidx.compose.ui.unit.sp + + +@Composable +fun MoreButton( + onMoreClick: () -> Unit +) { + Row( + modifier = Modifier + .clickable(onClick = onMoreClick), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "더보기", + fontSize = 14.sp, + color = Color.Gray + ) + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = "관광지 더보기", + modifier = Modifier + .size(20.dp), + tint = Color.Gray + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt index 95a5652..8470d14 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt @@ -1,5 +1,6 @@ package kr.ksw.visitkorea.presentation.home.screen +import android.content.Intent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,11 +16,14 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -38,11 +42,15 @@ import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import coil.request.ImageRequest import coil.size.Size +import kotlinx.coroutines.flow.collectLatest import kr.ksw.visitkorea.presentation.home.component.CultureCard +import kr.ksw.visitkorea.presentation.home.component.MoreButton import kr.ksw.visitkorea.presentation.home.component.RestaurantCard import kr.ksw.visitkorea.presentation.home.component.TouristSpotCard import kr.ksw.visitkorea.presentation.home.viewmodel.HomeState +import kr.ksw.visitkorea.presentation.home.viewmodel.HomeUiEffect import kr.ksw.visitkorea.presentation.home.viewmodel.HomeViewModel +import kr.ksw.visitkorea.presentation.more.MoreActivity import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme @Composable @@ -50,14 +58,32 @@ fun HomeScreen( homeViewModel: HomeViewModel = hiltViewModel() ) { val homeState by homeViewModel.homeState.collectAsState() + val context = LocalContext.current + LaunchedEffect(homeViewModel.homeUiEffect) { + homeViewModel.homeUiEffect.collectLatest { effect -> + when(effect) { + is HomeUiEffect.StartHomeActivity -> { + context.startActivity(Intent( + context, + MoreActivity::class.java + ).apply { + putExtra("contentTypeId", effect.contentTypeId) + }) + } + } + } + } + HomeScreen( - homeState = homeState + homeState = homeState, + onMoreClick = homeViewModel::startMoreActivity ) } @Composable fun HomeScreen( - homeState: HomeState + homeState: HomeState, + onMoreClick: (String) -> Unit ) { val scrollState = rememberScrollState() Surface { @@ -125,15 +151,24 @@ fun HomeScreen( Column( modifier = Modifier .fillMaxWidth() - .padding(top = 16.dp) + .padding(top = 16.dp, bottom = 20.dp) ) { - Text( + Row( modifier = Modifier - .padding(start = 16.dp, bottom = 20.dp), - text = "관광지", - fontSize = 22.sp, - fontWeight = FontWeight.Medium - ) + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "관광지", + fontSize = 22.sp, + fontWeight = FontWeight.Medium + ) + MoreButton { + onMoreClick("12") + } + } LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 16.dp) @@ -158,13 +193,22 @@ fun HomeScreen( .fillMaxWidth() .padding(top = 16.dp, bottom = 20.dp) ) { - Text( + Row( modifier = Modifier - .padding(start = 16.dp, bottom = 10.dp), - text = "문화시설", - fontSize = 22.sp, - fontWeight = FontWeight.Medium - ) + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "문화시설", + fontSize = 22.sp, + fontWeight = FontWeight.Medium + ) + MoreButton { + onMoreClick("14") + } + } LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 16.dp) @@ -189,13 +233,22 @@ fun HomeScreen( .fillMaxWidth() .padding(top = 16.dp, bottom = 20.dp) ) { - Text( + Row( modifier = Modifier - .padding(start = 16.dp, bottom = 10.dp), - text = "레포츠", - fontSize = 22.sp, - fontWeight = FontWeight.Medium - ) + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "레포츠", + fontSize = 22.sp, + fontWeight = FontWeight.Medium + ) + MoreButton { + onMoreClick("28") + } + } LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 16.dp) @@ -220,13 +273,22 @@ fun HomeScreen( .fillMaxWidth() .padding(top = 16.dp, bottom = 20.dp) ) { - Text( + Row( modifier = Modifier - .padding(start = 16.dp, bottom = 10.dp), - text = "음식점", - fontSize = 22.sp, - fontWeight = FontWeight.Medium - ) + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "음식점", + fontSize = 22.sp, + fontWeight = FontWeight.Medium + ) + MoreButton { + onMoreClick("39") + } + } LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 16.dp) @@ -257,7 +319,8 @@ fun HomeScreenPreview() { HomeScreen( homeState = HomeState( mainImage = "https://tong.visitkorea.or.kr/cms/resource/11/3094511_image2_1.jpg" - ) + ), + onMoreClick = { } ) } } diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeUiEffect.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeUiEffect.kt new file mode 100644 index 0000000..ed90692 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeUiEffect.kt @@ -0,0 +1,5 @@ +package kr.ksw.visitkorea.presentation.home.viewmodel + +sealed class HomeUiEffect { + data class StartHomeActivity(val contentTypeId: String) : HomeUiEffect() +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeViewModel.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeViewModel.kt index f7bd211..19494b8 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeViewModel.kt @@ -3,8 +3,11 @@ package kr.ksw.visitkorea.presentation.home.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -25,6 +28,10 @@ class HomeViewModel @Inject constructor( val homeState: StateFlow get() = _homeState.asStateFlow() + private val _homeUiEffect = MutableSharedFlow(replay = 0) + val homeUiEffect: SharedFlow + get() = _homeUiEffect.asSharedFlow() + init { getTouristSpot() getCultureCenter() @@ -96,4 +103,10 @@ class HomeViewModel @Inject constructor( } } } + + fun startMoreActivity(contentTypeId: String) { + viewModelScope.launch { + _homeUiEffect.emit(HomeUiEffect.StartHomeActivity(contentTypeId)) + } + } } \ No newline at end of file From 5713844d9d9a37367163f4230a58258c850ed005 Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Wed, 2 Oct 2024 19:09:40 +0900 Subject: [PATCH 06/11] feat: add MoreActivity & Screen --- app/src/main/AndroidManifest.xml | 1 + .../presentation/more/MoreActivity.kt | 30 ++++++ .../more/component/MoreScreenHeader.kt | 67 ++++++++++++ .../more/component/MoreTouristCard.kt | 8 ++ .../presentation/more/screen/MoreScreen.kt | 100 ++++++++++++++++++ .../presentation/more/viewmodel/MoreState.kt | 12 +++ .../more/viewmodel/MoreViewModel.kt | 53 ++++++++++ app/src/main/res/values/strings.xml | 5 + 8 files changed, 276 insertions(+) create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/more/MoreActivity.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreScreenHeader.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreTouristCard.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreState.kt create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index acdcef2..bbb744f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -41,6 +41,7 @@ + \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/MoreActivity.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/MoreActivity.kt new file mode 100644 index 0000000..1cf747e --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/MoreActivity.kt @@ -0,0 +1,30 @@ +package kr.ksw.visitkorea.presentation.more + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import dagger.hilt.android.AndroidEntryPoint +import kr.ksw.visitkorea.presentation.more.screen.MoreScreen +import kr.ksw.visitkorea.presentation.more.viewmodel.MoreViewModel +import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme + +@AndroidEntryPoint +class MoreActivity : ComponentActivity() { + private val viewModel: MoreViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + VisitKoreaTheme { + MoreScreen( + viewModel = viewModel, + contentTypeId = intent.getStringExtra("contentTypeId") ?: "" + ) { + finish() + } + } + } + viewModel.getMoreListByContentType(intent.getStringExtra("contentTypeId") ?: "") + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreScreenHeader.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreScreenHeader.kt new file mode 100644 index 0000000..0e8d012 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreScreenHeader.kt @@ -0,0 +1,67 @@ +package kr.ksw.visitkorea.presentation.more.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme + +@Composable +fun MoreScreenHeader( + title: String, + onBackButtonClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = 4.dp + ), + ) { + IconButton( + onClick = onBackButtonClick + ) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back Button", + modifier = Modifier + .size(40.dp) + .align(Alignment.CenterStart) + ) + } + Text( + modifier = Modifier + .align(Alignment.Center), + text = title, + fontWeight = FontWeight.Medium, + fontSize = 24.sp + ) + } +} + +@Composable +@Preview +fun MoreScreenHeaderPreview() { + VisitKoreaTheme { + Surface { + MoreScreenHeader( + "관광지" + ) { + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreTouristCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreTouristCard.kt new file mode 100644 index 0000000..5ea56ee --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreTouristCard.kt @@ -0,0 +1,8 @@ +package kr.ksw.visitkorea.presentation.more.component + +import androidx.compose.runtime.Composable + +@Composable +fun MoreTouristCard() { + +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt new file mode 100644 index 0000000..9d66fa6 --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt @@ -0,0 +1,100 @@ +package kr.ksw.visitkorea.presentation.more.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import kr.ksw.visitkorea.R +import kr.ksw.visitkorea.domain.usecase.model.MoreCardModel +import kr.ksw.visitkorea.presentation.home.component.CultureCard +import kr.ksw.visitkorea.presentation.more.component.MoreScreenHeader +import kr.ksw.visitkorea.presentation.more.viewmodel.MoreViewModel +import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme + +@Composable +fun MoreScreen( + viewModel: MoreViewModel, + contentTypeId: String, + onBackButtonClick: () -> Unit +) { + val moreState by viewModel.moreState.collectAsState() + val moreCardModels = moreState.moreCardModelFlow.collectAsLazyPagingItems() + MoreScreen( + moreCardModels = moreCardModels, + contentTypeId, + onBackButtonClick + ) +} + +@Composable +fun MoreScreen( + moreCardModels: LazyPagingItems, + contentTypeId: String, + onBackButtonClick: () -> Unit +) { + Surface { + Column( + modifier = Modifier + .fillMaxSize() + ) { + MoreScreenHeader( + title = stringResource(moreTitle(contentTypeId)), + onBackButtonClick = onBackButtonClick + ) + LazyVerticalGrid( + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + items( + count = moreCardModels.itemCount, + key = { index -> + moreCardModels[index]?.contentId?.toInt() ?: index + } + ) { index -> + val hotel = moreCardModels[index] + hotel?.run { + CultureCard( + title = hotel.title, + address = hotel.address, + image = hotel.firstImage + ) + } + } + } + } + } +} + +private fun moreTitle(contentTypeId: String) = when(contentTypeId) { + "12" -> R.string.tourist_spot_title + "14" -> R.string.culture_center_title + "28" -> R.string.leisure_sports_title + else -> R.string.restaurant_title +} + +@Composable +@Preview(showBackground = true) +fun MoreScreenPreview() { + VisitKoreaTheme { + MoreScreen( + viewModel = hiltViewModel(), + contentTypeId = "12" + ) { } + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreState.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreState.kt new file mode 100644 index 0000000..430540c --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreState.kt @@ -0,0 +1,12 @@ +package kr.ksw.visitkorea.presentation.more.viewmodel + +import androidx.compose.runtime.Immutable +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kr.ksw.visitkorea.domain.usecase.model.MoreCardModel + +@Immutable +data class MoreState( + val moreCardModelFlow: Flow> = emptyFlow(), +) diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt new file mode 100644 index 0000000..621e6dc --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt @@ -0,0 +1,53 @@ +package kr.ksw.visitkorea.presentation.more.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import androidx.paging.map +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kr.ksw.visitkorea.R +import kr.ksw.visitkorea.domain.usecase.mapper.toMoreCardModel +import kr.ksw.visitkorea.domain.usecase.more.GetMoreListUseCase +import javax.inject.Inject + +@HiltViewModel +class MoreViewModel @Inject constructor( + private val getMoreListUseCase: GetMoreListUseCase +): ViewModel() { + private val _moreState = MutableStateFlow(MoreState()) + val moreState: StateFlow + get() = _moreState.asStateFlow() + + fun getMoreListByContentType( + contentTypeId: String, + forceFetch: Boolean = false + ) { + viewModelScope.launch { + val moreListFlow = getMoreListUseCase( + forceFetch, + "126.9817290217", + "37.5678958128", + contentTypeId + ).getOrNull() + if(moreListFlow != null) { + val moreCardModelFlow = moreListFlow.map { pagingData -> + pagingData.map { + it.toMoreCardModel() + } + }.cachedIn(viewModelScope) + _moreState.update { + it.copy( + moreCardModelFlow = moreCardModelFlow + ) + } + } + } + } + +} \ 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 c1a4cd4..7b9b8e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,4 +8,9 @@ 행사 검색 즐겨찾기 + + 관광지 + 문화시설 + 레포츠 + 음식점 \ No newline at end of file From 726dbc9b5a016cc1df2bc8a0ab59dfbf8e14e0a8 Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Thu, 3 Oct 2024 17:00:05 +0900 Subject: [PATCH 07/11] feat: add pull refresh ui to MoreScreen --- .../presentation/component/SingleLineText.kt | 8 +- .../more/component/MoreTouristCard.kt | 130 +++++++++++++++++- .../presentation/more/screen/MoreScreen.kt | 73 +++++++--- .../presentation/more/viewmodel/MoreState.kt | 1 + .../more/viewmodel/MoreViewModel.kt | 30 ++-- 5 files changed, 211 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/component/SingleLineText.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/component/SingleLineText.kt index 7b105b8..5c0020c 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/component/SingleLineText.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/component/SingleLineText.kt @@ -1,12 +1,16 @@ package kr.ksw.visitkorea.presentation.component +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp @Composable fun SingleLineText( @@ -15,6 +19,7 @@ fun SingleLineText( fontSize: TextUnit, fontWeight: FontWeight = FontWeight.Normal, color: Color = Color.Black, + style: TextStyle = LocalTextStyle.current ) { Text( modifier = modifier, @@ -23,6 +28,7 @@ fun SingleLineText( fontWeight = fontWeight, color = color, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + style = style ) } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreTouristCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreTouristCard.kt index 5ea56ee..52f2f47 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreTouristCard.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/component/MoreTouristCard.kt @@ -1,8 +1,136 @@ package kr.ksw.visitkorea.presentation.more.component +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.LocationOn +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CardElevation +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import coil.size.Size +import kr.ksw.visitkorea.presentation.component.SingleLineText +import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme @Composable -fun MoreTouristCard() { +fun MoreTouristCard( + title: String, + address: String, + image: String, +) { + Card( + modifier = Modifier + .aspectRatio(0.7f), + shape = RoundedCornerShape(20.dp), + elevation = CardDefaults.elevatedCardElevation(6.dp) + ) { + Box { + AsyncImage( + modifier = Modifier + .fillMaxSize() + .background(color = Color.LightGray), + model = ImageRequest + .Builder(LocalContext.current) + .data(image) + .size(Size.ORIGINAL) + .build(), + contentDescription = "Tourist Card", + contentScale = ContentScale.Crop, + ) + Icon( + Icons.Outlined.FavoriteBorder, + contentDescription = "Favorite Icon", + modifier = Modifier + .align(Alignment.TopEnd) + .padding(10.dp) + .size(24.dp), + tint = Color.Red + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(8.dp)) + .background(Color.White) + .padding(vertical = 6.dp, horizontal = 4.dp) + ) { + SingleLineText( + modifier = Modifier + .padding(start = 4.dp), + text = title, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + style = TextStyle( + platformStyle = PlatformTextStyle( + includeFontPadding = false + ) + ) + ) + Spacer(modifier = Modifier.height(4.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .size(18.dp), + imageVector = Icons.Outlined.LocationOn, + contentDescription = null, + ) + SingleLineText( + modifier = Modifier.weight(1f), + text = address, + fontSize = 12.sp, + style = TextStyle( + platformStyle = PlatformTextStyle( + includeFontPadding = false + ) + ) + ) + } + } + } + } +} +@Composable +@Preview(showBackground = true) +fun MoreTouristCard() { + VisitKoreaTheme { + Surface { + MoreTouristCard( + "수원화성", + "수원시 장안구 OOO", + "" + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt index 9d66fa6..e742ea3 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt @@ -1,16 +1,26 @@ package kr.ksw.visitkorea.presentation.more.screen +import android.util.Log import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Surface +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -18,13 +28,17 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kr.ksw.visitkorea.R import kr.ksw.visitkorea.domain.usecase.model.MoreCardModel import kr.ksw.visitkorea.presentation.home.component.CultureCard import kr.ksw.visitkorea.presentation.more.component.MoreScreenHeader +import kr.ksw.visitkorea.presentation.more.component.MoreTouristCard import kr.ksw.visitkorea.presentation.more.viewmodel.MoreViewModel import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreScreen( viewModel: MoreViewModel, @@ -33,15 +47,28 @@ fun MoreScreen( ) { val moreState by viewModel.moreState.collectAsState() val moreCardModels = moreState.moreCardModelFlow.collectAsLazyPagingItems() + val state = rememberPullToRefreshState() + + val onRefresh = { + viewModel.getMoreListByContentType(contentTypeId, true) + } + MoreScreen( + state, + moreState.isRefreshing, + onRefresh, moreCardModels = moreCardModels, contentTypeId, onBackButtonClick ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreScreen( + state: PullToRefreshState, + isRefreshing: Boolean, + onRefresh: () -> Unit, moreCardModels: LazyPagingItems, contentTypeId: String, onBackButtonClick: () -> Unit @@ -55,25 +82,33 @@ fun MoreScreen( title = stringResource(moreTitle(contentTypeId)), onBackButtonClick = onBackButtonClick ) - LazyVerticalGrid( - columns = GridCells.Fixed(2), - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), + PullToRefreshBox( + modifier = Modifier + .fillMaxSize(), + state = state, + isRefreshing = isRefreshing, + onRefresh = onRefresh, ) { - items( - count = moreCardModels.itemCount, - key = { index -> - moreCardModels[index]?.contentId?.toInt() ?: index - } - ) { index -> - val hotel = moreCardModels[index] - hotel?.run { - CultureCard( - title = hotel.title, - address = hotel.address, - image = hotel.firstImage - ) + LazyVerticalGrid( + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items( + count = moreCardModels.itemCount, + key = { index -> + moreCardModels[index]?.contentId?.toInt() ?: index + } + ) { index -> + val hotel = moreCardModels[index] + hotel?.run { + MoreTouristCard( + title = hotel.title, + address = hotel.address, + image = hotel.firstImage + ) + } } } } diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreState.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreState.kt index 430540c..6ac98d3 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreState.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreState.kt @@ -8,5 +8,6 @@ import kr.ksw.visitkorea.domain.usecase.model.MoreCardModel @Immutable data class MoreState( + val isRefreshing: Boolean = true, val moreCardModelFlow: Flow> = emptyFlow(), ) diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt index 621e6dc..36bbf7d 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt @@ -28,6 +28,11 @@ class MoreViewModel @Inject constructor( contentTypeId: String, forceFetch: Boolean = false ) { + if(forceFetch) { + _moreState.update { + it.copy(isRefreshing = true) + } + } viewModelScope.launch { val moreListFlow = getMoreListUseCase( forceFetch, @@ -35,19 +40,24 @@ class MoreViewModel @Inject constructor( "37.5678958128", contentTypeId ).getOrNull() - if(moreListFlow != null) { - val moreCardModelFlow = moreListFlow.map { pagingData -> - pagingData.map { - it.toMoreCardModel() - } - }.cachedIn(viewModelScope) + if(moreListFlow == null) { + // Toast Effect _moreState.update { - it.copy( - moreCardModelFlow = moreCardModelFlow - ) + it.copy(isRefreshing = false) } + return@launch + } + val moreCardModelFlow = moreListFlow.map { pagingData -> + pagingData.map { + it.toMoreCardModel() + } + }.cachedIn(viewModelScope) + _moreState.update { + it.copy( + moreCardModelFlow = moreCardModelFlow, + isRefreshing = false + ) } } } - } \ No newline at end of file From 7ea3029366e693555da5fd8524181df3ea653546 Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Thu, 3 Oct 2024 17:01:00 +0900 Subject: [PATCH 08/11] fix: fix network response parsing exception --- .../source/LocationBasedPagingSource.kt | 27 +++++++++++-------- .../source/SearchFestivalPagingSource.kt | 4 ++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/LocationBasedPagingSource.kt b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/LocationBasedPagingSource.kt index 0376d8f..08aa503 100644 --- a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/LocationBasedPagingSource.kt +++ b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/LocationBasedPagingSource.kt @@ -23,19 +23,24 @@ class LocationBasedPagingSource ( override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: 1 val loadSize = params.loadSize - val response = locationBasedListApi.getLocationBasedListByContentType( - numOfRows = loadSize, - pageNo = page, - mapX = mapX, - mapY = mapY, - contentTypeId = contentTypeId - ) - val data = response.toItems() + val data = try { + locationBasedListApi.getLocationBasedListByContentType( + numOfRows = loadSize, + pageNo = page, + mapX = mapX, + mapY = mapY, + contentTypeId = contentTypeId + ).toItems() + } catch (e: Exception) { + emptyList() + } return LoadResult.Page( data = data, - prevKey = if(page == 1) null else page - 1, - nextKey = if(data.size == loadSize) page + 1 else null + prevKey = if(page == 1 || data.isEmpty()) null else page - 1, + nextKey = if(data.size == loadSize && + data.isNotEmpty() && + page * loadSize < 100 + ) page + 1 else null ) } - } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt index cafa1a8..307e835 100644 --- a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt +++ b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt @@ -36,7 +36,9 @@ class SearchFestivalPagingSource( return LoadResult.Page( data = data, prevKey = if(page == 1) null else page - 1, - nextKey = if(data.size == loadSize) page + 1 else null + nextKey = if(data.size == loadSize && page * loadSize < 50) + page + 1 + else null ) } } \ No newline at end of file From 547f05e7125d431f8814ac330a1157adfbbb21bd Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Thu, 3 Oct 2024 17:05:28 +0900 Subject: [PATCH 09/11] design: Card & List design changed --- .../visitkorea/presentation/festival/screen/FestivalScreen.kt | 2 ++ .../ksw/visitkorea/presentation/home/component/CultureCard.kt | 3 +-- .../visitkorea/presentation/home/component/RestaurantCard.kt | 4 ++-- .../kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/festival/screen/FestivalScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/screen/FestivalScreen.kt index b61e082..bf7bb4a 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/festival/screen/FestivalScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/festival/screen/FestivalScreen.kt @@ -2,6 +2,7 @@ package kr.ksw.visitkorea.presentation.festival.screen import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -86,6 +87,7 @@ fun FestivalScreen( ) Spacer(modifier = Modifier.height(16.dp)) LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { items( diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/CultureCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/CultureCard.kt index f327d49..69f5399 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/CultureCard.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/CultureCard.kt @@ -35,14 +35,13 @@ import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme @Composable fun CultureCard( modifier: Modifier = Modifier, - ratio: Float = 0.7f, title: String, address: String, image: String, ) { Card ( modifier = modifier - .aspectRatio(ratio), + .aspectRatio(0.7f), shape = RoundedCornerShape(24.dp), elevation = CardDefaults.elevatedCardElevation( defaultElevation = 4.dp diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt index e9bd8c2..95f5ba7 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt @@ -40,10 +40,10 @@ fun RestaurantCard( dist: String, category: String, image: String, + modifier: Modifier = Modifier ) { Card( - modifier = Modifier - .width(300.dp), + modifier = modifier, elevation = CardDefaults.elevatedCardElevation(6.dp), ) { Row( diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt index 8470d14..476ba71 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -303,7 +304,8 @@ fun HomeScreen( restaurant.address, restaurant.dist, restaurant.category, - restaurant.firstImage + restaurant.firstImage, + Modifier.width(300.dp) ) } } From 7d57b1c76e3e9b8d6961dff37e506d18861831ad Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Thu, 3 Oct 2024 17:29:17 +0900 Subject: [PATCH 10/11] refactor: add ContentType Enum Class --- .../presentation/common/ContentType.kt | 14 +++++ .../presentation/home/screen/HomeScreen.kt | 14 +++-- .../home/viewmodel/HomeUiEffect.kt | 4 +- .../home/viewmodel/HomeViewModel.kt | 5 +- .../presentation/more/MoreActivity.kt | 6 +- .../presentation/more/screen/MoreScreen.kt | 61 ++++++++++--------- 6 files changed, 64 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/kr/ksw/visitkorea/presentation/common/ContentType.kt diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/common/ContentType.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/common/ContentType.kt new file mode 100644 index 0000000..fde523f --- /dev/null +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/common/ContentType.kt @@ -0,0 +1,14 @@ +package kr.ksw.visitkorea.presentation.common + +import androidx.annotation.StringRes +import kr.ksw.visitkorea.R + +enum class ContentType( + val contentTypeId: String, + @StringRes val title: Int +) { + TOURIST("12", R.string.tourist_spot_title), + CULTURE("14", R.string.culture_center_title), + LEiSURE("28", R.string.leisure_sports_title), + RESTAURANT("39", R.string.restaurant_title) +} \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt index 476ba71..c1af239 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/screen/HomeScreen.kt @@ -44,6 +44,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import coil.size.Size import kotlinx.coroutines.flow.collectLatest +import kr.ksw.visitkorea.presentation.common.ContentType import kr.ksw.visitkorea.presentation.home.component.CultureCard import kr.ksw.visitkorea.presentation.home.component.MoreButton import kr.ksw.visitkorea.presentation.home.component.RestaurantCard @@ -51,6 +52,7 @@ import kr.ksw.visitkorea.presentation.home.component.TouristSpotCard import kr.ksw.visitkorea.presentation.home.viewmodel.HomeState import kr.ksw.visitkorea.presentation.home.viewmodel.HomeUiEffect import kr.ksw.visitkorea.presentation.home.viewmodel.HomeViewModel +import kr.ksw.visitkorea.presentation.main.MainRoute import kr.ksw.visitkorea.presentation.more.MoreActivity import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme @@ -68,7 +70,7 @@ fun HomeScreen( context, MoreActivity::class.java ).apply { - putExtra("contentTypeId", effect.contentTypeId) + putExtra("contentType", effect.contentType) }) } } @@ -84,7 +86,7 @@ fun HomeScreen( @Composable fun HomeScreen( homeState: HomeState, - onMoreClick: (String) -> Unit + onMoreClick: (ContentType) -> Unit ) { val scrollState = rememberScrollState() Surface { @@ -167,7 +169,7 @@ fun HomeScreen( fontWeight = FontWeight.Medium ) MoreButton { - onMoreClick("12") + onMoreClick(ContentType.TOURIST) } } LazyRow( @@ -207,7 +209,7 @@ fun HomeScreen( fontWeight = FontWeight.Medium ) MoreButton { - onMoreClick("14") + onMoreClick(ContentType.CULTURE) } } LazyRow( @@ -247,7 +249,7 @@ fun HomeScreen( fontWeight = FontWeight.Medium ) MoreButton { - onMoreClick("28") + onMoreClick(ContentType.LEiSURE) } } LazyRow( @@ -287,7 +289,7 @@ fun HomeScreen( fontWeight = FontWeight.Medium ) MoreButton { - onMoreClick("39") + onMoreClick(ContentType.RESTAURANT) } } LazyRow( diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeUiEffect.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeUiEffect.kt index ed90692..7b4d046 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeUiEffect.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeUiEffect.kt @@ -1,5 +1,7 @@ package kr.ksw.visitkorea.presentation.home.viewmodel +import kr.ksw.visitkorea.presentation.common.ContentType + sealed class HomeUiEffect { - data class StartHomeActivity(val contentTypeId: String) : HomeUiEffect() + data class StartHomeActivity(val contentType: ContentType) : HomeUiEffect() } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeViewModel.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeViewModel.kt index 19494b8..1566052 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/viewmodel/HomeViewModel.kt @@ -15,6 +15,7 @@ import kr.ksw.visitkorea.domain.usecase.home.GetCultureCenterForHomeUseCase import kr.ksw.visitkorea.domain.usecase.home.GetLeisureSportsForHomeUseCase import kr.ksw.visitkorea.domain.usecase.home.GetRestaurantForHomeUseCase import kr.ksw.visitkorea.domain.usecase.home.GetTouristSpotForHomeUseCase +import kr.ksw.visitkorea.presentation.common.ContentType import javax.inject.Inject @HiltViewModel @@ -104,9 +105,9 @@ class HomeViewModel @Inject constructor( } } - fun startMoreActivity(contentTypeId: String) { + fun startMoreActivity(contentType: ContentType) { viewModelScope.launch { - _homeUiEffect.emit(HomeUiEffect.StartHomeActivity(contentTypeId)) + _homeUiEffect.emit(HomeUiEffect.StartHomeActivity(contentType)) } } } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/MoreActivity.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/MoreActivity.kt index 1cf747e..db17e42 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/more/MoreActivity.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/MoreActivity.kt @@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import dagger.hilt.android.AndroidEntryPoint +import kr.ksw.visitkorea.presentation.common.ContentType import kr.ksw.visitkorea.presentation.more.screen.MoreScreen import kr.ksw.visitkorea.presentation.more.viewmodel.MoreViewModel import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme @@ -15,16 +16,17 @@ class MoreActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val contentType = (intent.getSerializableExtra("contentType") as? ContentType) ?: ContentType.TOURIST setContent { VisitKoreaTheme { MoreScreen( viewModel = viewModel, - contentTypeId = intent.getStringExtra("contentTypeId") ?: "" + contentType = contentType ) { finish() } } } - viewModel.getMoreListByContentType(intent.getStringExtra("contentTypeId") ?: "") + viewModel.getMoreListByContentType(contentType.contentTypeId) } } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt index e742ea3..4a46ef7 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt @@ -1,13 +1,12 @@ package kr.ksw.visitkorea.presentation.more.screen -import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Surface import androidx.compose.material3.pulltorefresh.PullToRefreshBox @@ -16,11 +15,6 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -28,11 +22,11 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kr.ksw.visitkorea.R import kr.ksw.visitkorea.domain.usecase.model.MoreCardModel +import kr.ksw.visitkorea.presentation.common.ContentType import kr.ksw.visitkorea.presentation.home.component.CultureCard +import kr.ksw.visitkorea.presentation.home.component.RestaurantCard import kr.ksw.visitkorea.presentation.more.component.MoreScreenHeader import kr.ksw.visitkorea.presentation.more.component.MoreTouristCard import kr.ksw.visitkorea.presentation.more.viewmodel.MoreViewModel @@ -42,7 +36,7 @@ import kr.ksw.visitkorea.presentation.ui.theme.VisitKoreaTheme @Composable fun MoreScreen( viewModel: MoreViewModel, - contentTypeId: String, + contentType: ContentType, onBackButtonClick: () -> Unit ) { val moreState by viewModel.moreState.collectAsState() @@ -50,7 +44,7 @@ fun MoreScreen( val state = rememberPullToRefreshState() val onRefresh = { - viewModel.getMoreListByContentType(contentTypeId, true) + viewModel.getMoreListByContentType(contentType.contentTypeId, true) } MoreScreen( @@ -58,7 +52,7 @@ fun MoreScreen( moreState.isRefreshing, onRefresh, moreCardModels = moreCardModels, - contentTypeId, + contentType, onBackButtonClick ) } @@ -70,7 +64,7 @@ fun MoreScreen( isRefreshing: Boolean, onRefresh: () -> Unit, moreCardModels: LazyPagingItems, - contentTypeId: String, + contentType: ContentType, onBackButtonClick: () -> Unit ) { Surface { @@ -79,7 +73,7 @@ fun MoreScreen( .fillMaxSize() ) { MoreScreenHeader( - title = stringResource(moreTitle(contentTypeId)), + title = stringResource(contentType.title), onBackButtonClick = onBackButtonClick ) PullToRefreshBox( @@ -101,13 +95,29 @@ fun MoreScreen( moreCardModels[index]?.contentId?.toInt() ?: index } ) { index -> - val hotel = moreCardModels[index] - hotel?.run { - MoreTouristCard( - title = hotel.title, - address = hotel.address, - image = hotel.firstImage - ) + val model = moreCardModels[index] + model?.run { + when(contentType) { + ContentType.TOURIST -> MoreTouristCard( + title = title, + address = address, + image = firstImage + ) + ContentType.CULTURE, + ContentType.LEiSURE -> CultureCard( + title = title, + address = address, + image = firstImage + ) + else -> RestaurantCard( + title = title, + address = address, + dist = dist, + category = category ?: "", + image = firstImage, + modifier = Modifier.fillMaxWidth() + ) + } } } } @@ -116,20 +126,13 @@ fun MoreScreen( } } -private fun moreTitle(contentTypeId: String) = when(contentTypeId) { - "12" -> R.string.tourist_spot_title - "14" -> R.string.culture_center_title - "28" -> R.string.leisure_sports_title - else -> R.string.restaurant_title -} - @Composable @Preview(showBackground = true) fun MoreScreenPreview() { VisitKoreaTheme { MoreScreen( viewModel = hiltViewModel(), - contentTypeId = "12" + contentType = ContentType.TOURIST ) { } } } \ No newline at end of file From 263f5252cde05b9d10dc95cb5bfb2abd5200258e Mon Sep 17 00:00:00 2001 From: ksw4015 Date: Sat, 5 Oct 2024 15:21:15 +0900 Subject: [PATCH 11/11] feat: add LazyColumn (Restaurant Type) to MoreScreen *change page size --- .../source/LocationBasedPagingSource.kt | 5 +- .../source/SearchFestivalPagingSource.kt | 2 +- .../festival/GetFestivalListUseCaseImpl.kt | 4 +- .../usecase/hotel/GetHotelListUseCaseImpl.kt | 4 +- .../usecase/more/GetMoreListUseCaseImpl.kt | 4 +- .../home/component/RestaurantCard.kt | 3 + .../presentation/more/screen/MoreScreen.kt | 72 ++++++++++++------- .../more/viewmodel/MoreViewModel.kt | 13 ++-- 8 files changed, 64 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/LocationBasedPagingSource.kt b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/LocationBasedPagingSource.kt index 08aa503..c4bf7b1 100644 --- a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/LocationBasedPagingSource.kt +++ b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/LocationBasedPagingSource.kt @@ -37,10 +37,7 @@ class LocationBasedPagingSource ( return LoadResult.Page( data = data, prevKey = if(page == 1 || data.isEmpty()) null else page - 1, - nextKey = if(data.size == loadSize && - data.isNotEmpty() && - page * loadSize < 100 - ) page + 1 else null + nextKey = if(data.size == loadSize && data.isNotEmpty()) page + 1 else null ) } } \ No newline at end of file diff --git a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt index 307e835..f0d3031 100644 --- a/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt +++ b/app/src/main/java/kr/ksw/visitkorea/data/paging/source/SearchFestivalPagingSource.kt @@ -36,7 +36,7 @@ class SearchFestivalPagingSource( return LoadResult.Page( data = data, prevKey = if(page == 1) null else page - 1, - nextKey = if(data.size == loadSize && page * loadSize < 50) + nextKey = if(data.size == loadSize) page + 1 else null ) diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCaseImpl.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCaseImpl.kt index 00ca36b..33ebfde 100644 --- a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCaseImpl.kt +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/festival/GetFestivalListUseCaseImpl.kt @@ -29,8 +29,8 @@ class GetFestivalListUseCaseImpl @Inject constructor( } Pager( config = PagingConfig( - pageSize = 10, - initialLoadSize = 10 + pageSize = 20, + initialLoadSize = 20 ), pagingSourceFactory = { SearchFestivalPagingSource( diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/hotel/GetHotelListUseCaseImpl.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/hotel/GetHotelListUseCaseImpl.kt index 5ed1ef6..1a07294 100644 --- a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/hotel/GetHotelListUseCaseImpl.kt +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/hotel/GetHotelListUseCaseImpl.kt @@ -27,8 +27,8 @@ class GetHotelListUseCaseImpl @Inject constructor( } Pager( config = PagingConfig( - pageSize = 10, - initialLoadSize = 10 + pageSize = 30, + initialLoadSize = 30 ), pagingSourceFactory = { LocationBasedPagingSource( diff --git a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCaseImpl.kt b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCaseImpl.kt index a587a76..7e8b5aa 100644 --- a/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCaseImpl.kt +++ b/app/src/main/java/kr/ksw/visitkorea/domain/usecase/more/GetMoreListUseCaseImpl.kt @@ -28,8 +28,8 @@ class GetMoreListUseCaseImpl @Inject constructor( } Pager( config = PagingConfig( - pageSize = 10, - initialLoadSize = 10 + pageSize = 30, + initialLoadSize = 30 ), pagingSourceFactory = { LocationBasedPagingSource( diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt index 95f5ba7..fbaecab 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/home/component/RestaurantCard.kt @@ -75,6 +75,9 @@ fun RestaurantCard( horizontalArrangement = Arrangement.SpaceBetween ) { SingleLineText( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp), text = title, fontSize = 16.sp, fontWeight = FontWeight.Medium diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt index 4a46ef7..0192130 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/screen/MoreScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material3.ExperimentalMaterial3Api @@ -83,33 +84,20 @@ fun MoreScreen( isRefreshing = isRefreshing, onRefresh = onRefresh, ) { - LazyVerticalGrid( - columns = GridCells.Fixed(2), - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - items( - count = moreCardModels.itemCount, - key = { index -> - moreCardModels[index]?.contentId?.toInt() ?: index - } - ) { index -> - val model = moreCardModels[index] - model?.run { - when(contentType) { - ContentType.TOURIST -> MoreTouristCard( - title = title, - address = address, - image = firstImage - ) - ContentType.CULTURE, - ContentType.LEiSURE -> CultureCard( - title = title, - address = address, - image = firstImage - ) - else -> RestaurantCard( + if(contentType == ContentType.RESTAURANT) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(16.dp), + ) { + items( + count = moreCardModels.itemCount, + key = { index -> + moreCardModels[index]?.contentId?.toInt() ?: index + } + ) { index -> + val model = moreCardModels[index] + model?.run { + RestaurantCard( title = title, address = address, dist = dist, @@ -120,6 +108,36 @@ fun MoreScreen( } } } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items( + count = moreCardModels.itemCount, + key = { index -> + moreCardModels[index]?.contentId?.toInt() ?: index + } + ) { index -> + val model = moreCardModels[index] + model?.run { + when(contentType) { + ContentType.TOURIST -> MoreTouristCard( + title = title, + address = address, + image = firstImage + ) + else -> CultureCard( + title = title, + address = address, + image = firstImage + ) + } + } + } + } } } } diff --git a/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt index 36bbf7d..31960f0 100644 --- a/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt +++ b/app/src/main/java/kr/ksw/visitkorea/presentation/more/viewmodel/MoreViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn import androidx.paging.map import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -28,12 +29,14 @@ class MoreViewModel @Inject constructor( contentTypeId: String, forceFetch: Boolean = false ) { - if(forceFetch) { - _moreState.update { - it.copy(isRefreshing = true) - } - } viewModelScope.launch { + if(forceFetch) { + _moreState.update { + it.copy(isRefreshing = true) + } + delay(500) + } + val moreListFlow = getMoreListUseCase( forceFetch, "126.9817290217",