From 8bd7f275e5a60b8ea109f9e518c46526df245b6c Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 12 Aug 2026 14:09:01 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20novel=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `data/novel/build.gradle.kts`: Retrofit, Paging, Serialization 등 의존성 설정 및 Android 라이브러리 모듈 환경 구성 - `settings.gradle.kts`: 전체 프로젝트 구성 내 `:data:novel` 모듈 추가 - `data/novel/src/main/AndroidManifest.xml`: 라이브러리 구성을 위한 기본 매니페스트 파일 추가 --- data/novel/build.gradle.kts | 20 ++++++++++++++++++++ data/novel/src/main/AndroidManifest.xml | 2 ++ settings.gradle.kts | 1 + 3 files changed, 23 insertions(+) create mode 100644 data/novel/build.gradle.kts create mode 100644 data/novel/src/main/AndroidManifest.xml diff --git a/data/novel/build.gradle.kts b/data/novel/build.gradle.kts new file mode 100644 index 000000000..2b6edc225 --- /dev/null +++ b/data/novel/build.gradle.kts @@ -0,0 +1,20 @@ +import com.into.websoso.setNamespace + +plugins { + id("websoso.android.library") + alias(libs.plugins.kotlin.serialization) +} + +android { + setNamespace("data.novel") +} + +dependencies { + implementation(projects.core.network) + + implementation(libs.paging.runtime) + implementation(libs.retrofit) + implementation(libs.serialization.json) + + testImplementation(libs.junit) +} diff --git a/data/novel/src/main/AndroidManifest.xml b/data/novel/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8072ee00d --- /dev/null +++ b/data/novel/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 81a0aa86b..e814f4de1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ include( include( ":data:account", ":data:library", + ":data:novel", ":data:feed", ) From a3e392bfb4ca65499ba5d5e8a78182b6ad8ab52c Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 12 Aug 2026 14:22:23 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `NovelSearchEntity.kt`: 작품 ID, 제목, 작가, 이미지 URL 정보를 포함하는 `NovelSearchEntity` 데이터 클래스 정의 --- .../into/websoso/data/novel/model/NovelSearchEntity.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 data/novel/src/main/java/com/into/websoso/data/novel/model/NovelSearchEntity.kt diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/model/NovelSearchEntity.kt b/data/novel/src/main/java/com/into/websoso/data/novel/model/NovelSearchEntity.kt new file mode 100644 index 000000000..f5ba6a98e --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/model/NovelSearchEntity.kt @@ -0,0 +1,8 @@ +package com.into.websoso.data.novel.model + +data class NovelSearchEntity( + val novelId: Long, + val title: String, + val author: String, + val imageUrl: String, +) From 9e36cdeaba36ba7d0c3aa28f0718d2574de6b025 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 12 Aug 2026 14:58:12 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=9D=91=EB=8B=B5=20=EB=AA=A8=EB=8D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/novel/NovelSearchResponseDto.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchResponseDto.kt diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchResponseDto.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchResponseDto.kt new file mode 100644 index 000000000..baa0f49c4 --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchResponseDto.kt @@ -0,0 +1,33 @@ +package com.into.websoso.data.novel + +import com.into.websoso.data.novel.model.NovelSearchEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class NovelSearchResponseDto( + @SerialName("isLoadable") + val isLoadable: Boolean, + @SerialName("novels") + val novels: List, +) { + @Serializable + data class NovelDto( + @SerialName("novelId") + val novelId: Long, + @SerialName("title") + val title: String, + @SerialName("author") + val author: String, + @SerialName("novelImage") + val imageUrl: String, + ) { + fun toData(): NovelSearchEntity = + NovelSearchEntity( + novelId = novelId, + title = title, + author = author, + imageUrl = imageUrl, + ) + } +} From 5ed8c9950694ec3b4d8f5ca418498088378d02f6 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 12 Aug 2026 15:02:43 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/into/websoso/data/novel/NovelSearchApi.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApi.kt diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApi.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApi.kt new file mode 100644 index 000000000..8eeed5e32 --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApi.kt @@ -0,0 +1,13 @@ +package com.into.websoso.data.novel + +import retrofit2.http.GET +import retrofit2.http.Query + +internal interface NovelSearchApi { + @GET("novels") + suspend fun getNovels( + @Query("query") query: String, + @Query("page") page: Int, + @Query("size") size: Int, + ): NovelSearchResponseDto +} From 2f7fbf3b55750dd7d2837f699f6fa56b07ec8f9a Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 12 Aug 2026 15:17:44 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../websoso/data/novel/NovelSearchApiModule.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApiModule.kt diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApiModule.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApiModule.kt new file mode 100644 index 000000000..f20c102a8 --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApiModule.kt @@ -0,0 +1,16 @@ +package com.into.websoso.data.novel + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object NovelSearchApiModule { + @Provides + @Singleton + fun provideNovelSearchApi(retrofit: Retrofit): NovelSearchApi = retrofit.create(NovelSearchApi::class.java) +} From 21f62c0eba0420a4f5952da387fd3e5d40a05b0f Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 12 Aug 2026 16:07:10 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20PagingSource=20=EA=B8=B0=EB=B3=B8=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/novel/NovelSearchPagingSource.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt new file mode 100644 index 000000000..b9dc3d3f9 --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt @@ -0,0 +1,32 @@ +package com.into.websoso.data.novel + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.into.websoso.data.novel.model.NovelSearchEntity + +internal class NovelSearchPagingSource( + private val query: String, + private val api: NovelSearchApi, +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult = + LoadResult.Page( + data = api.getNovels( + query = query, + page = params.key ?: INITIAL_PAGE, + size = params.loadSize, + ).novels.map(NovelSearchResponseDto.NovelDto::toData), + prevKey = null, + nextKey = null, + ) + + override fun getRefreshKey(state: PagingState): Int? = + state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.let { page -> + page.prevKey?.plus(1) ?: page.nextKey?.minus(1) + } + } + + private companion object { + const val INITIAL_PAGE = 0 + } +} From 592c04938fbbef65cb5362f5cb3f03252a7ae7a1 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 12 Aug 2026 16:21:57 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20Paging=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/novel/NovelSearchPagingSource.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt index b9dc3d3f9..5ad5f13e5 100644 --- a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt @@ -8,16 +8,20 @@ internal class NovelSearchPagingSource( private val query: String, private val api: NovelSearchApi, ) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult = - LoadResult.Page( - data = api.getNovels( + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: INITIAL_PAGE + val response = api.getNovels( query = query, - page = params.key ?: INITIAL_PAGE, + page = page, size = params.loadSize, - ).novels.map(NovelSearchResponseDto.NovelDto::toData), - prevKey = null, - nextKey = null, + ) + + return LoadResult.Page( + data = response.novels.map(NovelSearchResponseDto.NovelDto::toData), + prevKey = page.takeIf { it > INITIAL_PAGE }?.minus(1), + nextKey = (page + 1).takeIf { response.isLoadable }, ) + } override fun getRefreshKey(state: PagingState): Int? = state.anchorPosition?.let { anchorPosition -> From 9cf8a68e33fabd13caf7daaf07a55c5193afbb79 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 12 Aug 2026 17:46:21 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20Paging=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/novel/NovelSearchPagingSource.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt index 5ad5f13e5..e1e756bbc 100644 --- a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt @@ -8,20 +8,21 @@ internal class NovelSearchPagingSource( private val query: String, private val api: NovelSearchApi, ) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - val page = params.key ?: INITIAL_PAGE - val response = api.getNovels( + override suspend fun load(params: LoadParams): LoadResult = + runCatching { + val page = params.key ?: INITIAL_PAGE + val response = api.getNovels( query = query, page = page, size = params.loadSize, ) - return LoadResult.Page( - data = response.novels.map(NovelSearchResponseDto.NovelDto::toData), - prevKey = page.takeIf { it > INITIAL_PAGE }?.minus(1), - nextKey = (page + 1).takeIf { response.isLoadable }, - ) - } + LoadResult.Page( + data = response.novels.map(NovelSearchResponseDto.NovelDto::toData), + prevKey = page.takeIf { it > INITIAL_PAGE }?.minus(1), + nextKey = (page + 1).takeIf { response.isLoadable }, + ) + }.getOrElse(LoadResult::Error) override fun getRefreshKey(state: PagingState): Int? = state.anchorPosition?.let { anchorPosition ->