From 1bf86c21abd7456091b501c70558508c55a4873e Mon Sep 17 00:00:00 2001 From: m6z1 Date: Sat, 6 Jun 2026 15:07:02 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=ED=83=90?= =?UTF-8?q?=EC=83=89=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../into/websoso/data/mapper/KeywordMapper.kt | 10 ++++ .../websoso/data/remote/api/KeywordApi.kt | 4 ++ .../response/PopularKeywordsResponseDto.kt | 18 +++++++ .../data/repository/KeywordRepository.kt | 3 ++ .../ui/normalExplore/NormalExploreActivity.kt | 43 +++++++++++++++ .../normalExplore/NormalExploreViewModel.kt | 32 ++++++++++++ .../res/layout/activity_normal_explore.xml | 52 ++++++++++++++++++- core/resource/src/main/res/values/strings.xml | 1 + 8 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/into/websoso/data/remote/response/PopularKeywordsResponseDto.kt 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..a80a1e7f6 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,16 @@ 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 +24,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..85c2a790c 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,7 @@ 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..f8fc05f08 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 @@ -9,11 +9,17 @@ import android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH import android.view.inputmethod.InputMethodManager import androidx.activity.addCallback import androidx.activity.viewModels +import com.into.websoso.R.color.gray_300_52515F +import com.into.websoso.R.color.gray_50_F4F5F8 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.custom.WebsosoChip +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 @@ -186,6 +192,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 +257,28 @@ 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 -> + WebsosoChip(this@NormalExploreActivity) + .apply { + setWebsosoChipText(keyword.keywordName) + setWebsosoChipTextAppearance(body3) + setWebsosoChipTextColor(gray_300_52515F) + setWebsosoChipBackgroundColor(gray_50_F4F5F8) + setWebsosoChipPaddingVertical(KEYWORD_CHIP_VERTICAL_PADDING.toFloatPxFromDp()) + setWebsosoChipPaddingHorizontal(KEYWORD_CHIP_HORIZONTAL_PADDING.toFloatPxFromDp()) + setWebsosoChipRadius(KEYWORD_CHIP_RADIUS.toFloatPxFromDp()) + setOnWebsosoChipClick { navigateToDetailExploreResult(keyword) } + }.also { websosoChip -> keywordChipGroup.addChip(websosoChip) } + } } private fun updateView(uiState: NormalExploreUiState) { @@ -264,6 +304,9 @@ class NormalExploreActivity : BaseActivity(activit companion object { const val SEARCH_AUTHOR = "SEARCH_AUTHOR" + private const val KEYWORD_CHIP_RADIUS = 20f + private const val KEYWORD_CHIP_VERTICAL_PADDING = 7f + 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/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"> 최근 검색어 전체삭제 장르별 검색 + 키워드 검색 작품 제목, 작가를 검색하세요 From d1f81ef1b628ed829f6c1ef877fcd97fc336e5e5 Mon Sep 17 00:00:00 2001 From: m6z1 Date: Sat, 6 Jun 2026 15:15:51 +0900 Subject: [PATCH 2/3] =?UTF-8?q?style:=20ktlint=20=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/into/websoso/data/mapper/KeywordMapper.kt | 3 +-- .../java/com/into/websoso/data/repository/KeywordRepository.kt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) 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 a80a1e7f6..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 @@ -9,8 +9,7 @@ fun KeywordsResponseDto.toData(): CategoriesEntity = categories = categories.map { it.toData() }, ) -fun PopularKeywordsResponseDto.toData(): List = - keywords.map { it.toData() } +fun PopularKeywordsResponseDto.toData(): List = keywords.map { it.toData() } fun KeywordsResponseDto.CategoryResponseDto.toData(): CategoriesEntity.CategoryEntity = CategoriesEntity.CategoryEntity( 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 85c2a790c..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 @@ -12,6 +12,5 @@ class KeywordRepository ) { suspend fun fetchKeywords(keyword: String?): CategoriesEntity = keywordApi.getKeywords(keyword).toData() - suspend fun fetchPopularKeywords(): List = - keywordApi.getPopularKeywords().toData() + suspend fun fetchPopularKeywords(): List = keywordApi.getPopularKeywords().toData() } From fb9e9dbb50173a1ac9c41ac0d062cf2fdf8dcb60 Mon Sep 17 00:00:00 2001 From: m6z1 Date: Sat, 6 Jun 2026 15:25:02 +0900 Subject: [PATCH 3/3] =?UTF-8?q?style:=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=B9=A9=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/normalExplore/NormalExploreActivity.kt | 39 ++++++++++++------- .../bg_normal_explore_keyword_search_chip.xml | 5 +++ 2 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/drawable/bg_normal_explore_keyword_search_chip.xml 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 f8fc05f08..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,16 +5,18 @@ 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.color.gray_50_F4F5F8 +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.custom.WebsosoChip 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 @@ -38,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) { @@ -267,17 +270,26 @@ class NormalExploreActivity : BaseActivity(activit val keywordChipGroup = binding.wcgNormalExploreKeywordSearch keywordChipGroup.removeAllViews() keywordSearches.forEach { keyword -> - WebsosoChip(this@NormalExploreActivity) + TextView(this@NormalExploreActivity) .apply { - setWebsosoChipText(keyword.keywordName) - setWebsosoChipTextAppearance(body3) - setWebsosoChipTextColor(gray_300_52515F) - setWebsosoChipBackgroundColor(gray_50_F4F5F8) - setWebsosoChipPaddingVertical(KEYWORD_CHIP_VERTICAL_PADDING.toFloatPxFromDp()) - setWebsosoChipPaddingHorizontal(KEYWORD_CHIP_HORIZONTAL_PADDING.toFloatPxFromDp()) - setWebsosoChipRadius(KEYWORD_CHIP_RADIUS.toFloatPxFromDp()) - setOnWebsosoChipClick { navigateToDetailExploreResult(keyword) } - }.also { websosoChip -> keywordChipGroup.addChip(websosoChip) } + 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) } } } @@ -304,8 +316,7 @@ class NormalExploreActivity : BaseActivity(activit companion object { const val SEARCH_AUTHOR = "SEARCH_AUTHOR" - private const val KEYWORD_CHIP_RADIUS = 20f - private const val KEYWORD_CHIP_VERTICAL_PADDING = 7f + private const val KEYWORD_CHIP_HEIGHT = 35f private const val KEYWORD_CHIP_HORIZONTAL_PADDING = 13f fun getIntent( 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 @@ + + + + +