diff --git a/app/src/main/java/com/into/websoso/data/mapper/KeywordMapper.kt b/app/src/main/java/com/into/websoso/data/mapper/KeywordMapper.kt index 76d877a98..4eac3576f 100644 --- a/app/src/main/java/com/into/websoso/data/mapper/KeywordMapper.kt +++ b/app/src/main/java/com/into/websoso/data/mapper/KeywordMapper.kt @@ -2,12 +2,15 @@ package com.into.websoso.data.mapper import com.into.websoso.data.model.CategoriesEntity import com.into.websoso.data.remote.response.KeywordsResponseDto +import com.into.websoso.data.remote.response.PopularKeywordsResponseDto fun KeywordsResponseDto.toData(): CategoriesEntity = CategoriesEntity( categories = categories.map { it.toData() }, ) +fun PopularKeywordsResponseDto.toData(): List = keywords.map { it.toData() } + fun KeywordsResponseDto.CategoryResponseDto.toData(): CategoriesEntity.CategoryEntity = CategoriesEntity.CategoryEntity( categoryName = categoryName, @@ -20,3 +23,9 @@ fun KeywordsResponseDto.CategoryResponseDto.KeywordResponseDto.toData(): Categor keywordId = keywordId, keywordName = keywordName, ) + +fun PopularKeywordsResponseDto.KeywordResponseDto.toData(): CategoriesEntity.CategoryEntity.KeywordEntity = + CategoriesEntity.CategoryEntity.KeywordEntity( + keywordId = keywordId, + keywordName = keywordName, + ) diff --git a/app/src/main/java/com/into/websoso/data/remote/api/KeywordApi.kt b/app/src/main/java/com/into/websoso/data/remote/api/KeywordApi.kt index 42d827b8e..1f333d3ea 100644 --- a/app/src/main/java/com/into/websoso/data/remote/api/KeywordApi.kt +++ b/app/src/main/java/com/into/websoso/data/remote/api/KeywordApi.kt @@ -1,6 +1,7 @@ package com.into.websoso.data.remote.api import com.into.websoso.data.remote.response.KeywordsResponseDto +import com.into.websoso.data.remote.response.PopularKeywordsResponseDto import retrofit2.http.GET import retrofit2.http.Query @@ -9,4 +10,7 @@ interface KeywordApi { suspend fun getKeywords( @Query("query") keyword: String?, ): KeywordsResponseDto + + @GET("keywords/popular") + suspend fun getPopularKeywords(): PopularKeywordsResponseDto } diff --git a/app/src/main/java/com/into/websoso/data/remote/response/PopularKeywordsResponseDto.kt b/app/src/main/java/com/into/websoso/data/remote/response/PopularKeywordsResponseDto.kt new file mode 100644 index 000000000..6555071c8 --- /dev/null +++ b/app/src/main/java/com/into/websoso/data/remote/response/PopularKeywordsResponseDto.kt @@ -0,0 +1,18 @@ +package com.into.websoso.data.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PopularKeywordsResponseDto( + @SerialName("keywords") + val keywords: List, +) { + @Serializable + data class KeywordResponseDto( + @SerialName("keywordId") + val keywordId: Int, + @SerialName("keywordName") + val keywordName: String, + ) +} diff --git a/app/src/main/java/com/into/websoso/data/repository/KeywordRepository.kt b/app/src/main/java/com/into/websoso/data/repository/KeywordRepository.kt index 323bf46dc..edf584091 100644 --- a/app/src/main/java/com/into/websoso/data/repository/KeywordRepository.kt +++ b/app/src/main/java/com/into/websoso/data/repository/KeywordRepository.kt @@ -11,4 +11,6 @@ class KeywordRepository private val keywordApi: KeywordApi, ) { suspend fun fetchKeywords(keyword: String?): CategoriesEntity = keywordApi.getKeywords(keyword).toData() + + suspend fun fetchPopularKeywords(): List = keywordApi.getPopularKeywords().toData() } diff --git a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt index ec1f45301..53c551c8d 100644 --- a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt +++ b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt @@ -5,15 +5,23 @@ import android.content.Intent import android.content.Intent.ACTION_VIEW import android.net.Uri import android.os.Bundle +import android.view.Gravity +import android.view.ViewGroup import android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH import android.view.inputmethod.InputMethodManager +import android.widget.TextView import androidx.activity.addCallback import androidx.activity.viewModels +import com.into.websoso.R.color.gray_300_52515F +import com.into.websoso.R.drawable.bg_normal_explore_keyword_search_chip import com.into.websoso.R.layout.activity_normal_explore +import com.into.websoso.R.style.body3 import com.into.websoso.core.common.ui.base.BaseActivity +import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel.KeywordModel import com.into.websoso.core.common.ui.model.ResultFrom.NormalExploreBack import com.into.websoso.core.common.util.InfiniteScrollListener import com.into.websoso.core.common.util.SingleEventHandler +import com.into.websoso.core.common.util.toFloatPxFromDp import com.into.websoso.core.common.util.tracker.Tracker import com.into.websoso.core.resource.R.string.novel_inquire_link import com.into.websoso.databinding.ActivityNormalExploreBinding @@ -32,6 +40,7 @@ import com.into.websoso.ui.normalExplore.model.NormalExploreUiState import com.into.websoso.ui.novelDetail.NovelDetailActivity import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import kotlin.math.roundToInt @AndroidEntryPoint class NormalExploreActivity : BaseActivity(activity_normal_explore) { @@ -186,6 +195,18 @@ class NormalExploreActivity : BaseActivity(activit } } + private fun navigateToDetailExploreResult(keyword: KeywordModel) { + singleEventHandler.throttleFirst { + val intent = DetailExploreResultActivity.getIntent( + context = this, + detailExploreFilteredModel = DetailExploreFilteredModel( + keywordIds = listOf(keyword.keywordId), + ), + ) + startActivity(intent) + } + } + private fun navigateToNovelDetail(novelId: Long) { singleEventHandler.throttleFirst { val intent = NovelDetailActivity.getIntent(this, novelId) @@ -239,6 +260,37 @@ class NormalExploreActivity : BaseActivity(activit normalExploreViewModel.recentSearches.observe(this) { recentSearches -> recentSearchAdapter.submitList(recentSearches) } + + normalExploreViewModel.keywordSearches.observe(this) { keywordSearches -> + updateKeywordSearchChips(keywordSearches) + } + } + + private fun updateKeywordSearchChips(keywordSearches: List) { + val keywordChipGroup = binding.wcgNormalExploreKeywordSearch + keywordChipGroup.removeAllViews() + keywordSearches.forEach { keyword -> + TextView(this@NormalExploreActivity) + .apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + KEYWORD_CHIP_HEIGHT.toFloatPxFromDp().roundToInt(), + ) + text = keyword.keywordName + gravity = Gravity.CENTER + includeFontPadding = false + setTextAppearance(body3) + setTextColor(getColor(gray_300_52515F)) + setBackgroundResource(bg_normal_explore_keyword_search_chip) + setPadding( + KEYWORD_CHIP_HORIZONTAL_PADDING.toFloatPxFromDp().roundToInt(), + 0, + KEYWORD_CHIP_HORIZONTAL_PADDING.toFloatPxFromDp().roundToInt(), + 0, + ) + setOnClickListener { navigateToDetailExploreResult(keyword) } + }.also { keywordChip -> keywordChipGroup.addView(keywordChip) } + } } private fun updateView(uiState: NormalExploreUiState) { @@ -264,6 +316,8 @@ class NormalExploreActivity : BaseActivity(activit companion object { const val SEARCH_AUTHOR = "SEARCH_AUTHOR" + private const val KEYWORD_CHIP_HEIGHT = 35f + private const val KEYWORD_CHIP_HORIZONTAL_PADDING = 13f fun getIntent( context: Context, diff --git a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt index c28c3d89f..685ff9b83 100644 --- a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt @@ -5,7 +5,9 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel.KeywordModel import com.into.websoso.data.model.SosoPickEntity +import com.into.websoso.data.repository.KeywordRepository import com.into.websoso.data.repository.NovelRepository import com.into.websoso.domain.usecase.GetNormalExploreResultUseCase import com.into.websoso.ui.mapper.toUi @@ -22,6 +24,7 @@ class NormalExploreViewModel constructor( private val getNormalExploreResultUseCase: GetNormalExploreResultUseCase, private val novelRepository: NovelRepository, + private val keywordRepository: KeywordRepository, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { private val _uiState: MutableLiveData = @@ -55,9 +58,17 @@ class NormalExploreViewModel private val _isRecentSearchesVisible: MutableLiveData = MutableLiveData(false) val isRecentSearchesVisible: LiveData get() = _isRecentSearchesVisible + private val _keywordSearches: MutableLiveData> = + MutableLiveData(emptyList()) + val keywordSearches: LiveData> get() = _keywordSearches + + private val _isKeywordSearchesVisible: MutableLiveData = MutableLiveData(false) + val isKeywordSearchesVisible: LiveData get() = _isKeywordSearchesVisible + init { fetchSosoPicks() fetchRecentSearches() + fetchKeywordSearches() if (initialSearchWord.isNotBlank()) { updateSearchResult(isSearchButtonClick = true) } @@ -90,6 +101,19 @@ class NormalExploreViewModel } } + private fun fetchKeywordSearches() { + viewModelScope.launch { + runCatching { + keywordRepository.fetchPopularKeywords() + }.onSuccess { result -> + _keywordSearches.value = result + .map { keyword -> keyword.toUi() } + .take(MAX_KEYWORD_SEARCH_COUNT) + updateKeywordSearchesVisibility() + } + } + } + fun updateSearchWord(searchWord: String) { _searchWord.value = searchWord savedStateHandle[SEARCH_AUTHOR] = searchWord @@ -102,6 +126,7 @@ class NormalExploreViewModel if (isSearchButtonClick) { _isSosoPickVisible.value = false updateRecentSearchesVisibility() + updateKeywordSearchesVisibility() } viewModelScope.launch { _uiState.value = _uiState.value?.copy(loading = isSearchButtonClick) @@ -149,6 +174,7 @@ class NormalExploreViewModel _isNovelResultEmptyBoxVisibility.value = false _isSosoPickVisible.value = true updateRecentSearchesVisibility() + updateKeywordSearchesVisibility() } fun deleteRecentSearch(recentSearchId: Long) { @@ -180,7 +206,13 @@ class NormalExploreViewModel _isSosoPickVisible.value == true && _recentSearches.value.orEmpty().isNotEmpty() } + private fun updateKeywordSearchesVisibility() { + _isKeywordSearchesVisible.value = + _isSosoPickVisible.value == true && _keywordSearches.value.orEmpty().isNotEmpty() + } + companion object { private const val MAX_RECENT_SEARCH_COUNT = 30 + private const val MAX_KEYWORD_SEARCH_COUNT = 7 } } diff --git a/app/src/main/res/drawable/bg_normal_explore_keyword_search_chip.xml b/app/src/main/res/drawable/bg_normal_explore_keyword_search_chip.xml new file mode 100644 index 000000000..e0858545c --- /dev/null +++ b/app/src/main/res/drawable/bg_normal_explore_keyword_search_chip.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_normal_explore.xml b/app/src/main/res/layout/activity_normal_explore.xml index 602c47f83..c923a091f 100644 --- a/app/src/main/res/layout/activity_normal_explore.xml +++ b/app/src/main/res/layout/activity_normal_explore.xml @@ -233,6 +233,56 @@ tools:listitem="@layout/item_normal_explore_genre_search" /> + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/cl_normal_explore_keyword_search_section"> 최근 검색어 전체삭제 장르별 검색 + 키워드 검색 작품 제목, 작가를 검색하세요