Skip to content

Commit

Permalink
[Feature]#36 링크 관련 API 구현 및 링크 추가/수정 화면, 검색 화면에 연결 (#37)
Browse files Browse the repository at this point in the history
* [FEATURE] #36 링크 삭제, 링크 수정, 검색 링크 목록, 링크 상세 조회 API, Datasource, UseCase 구현

* [FEATURE] #36 검색화면 검색 API 연동

* [FEATURE] #36 검색화면 bottomSheet 내 포킷 목록 조회 API 연결

* [FEATURE] #36 검색화면의 link상세 bottomSheet 구현 및 기존 bottomSheet에 공유 제외 클릭 이벤트 연결

* [BASE] #36 data 모듈에 room, sharedPreferences 관련 세팅 및 데이터베이스 구성

* [FEATURE] #36 최근 검색어 관련 기능 구현

* [FEATURE] #36 즐겨찾기 등록/취소 API, datasource, useCase 구현

* [FEATURE] #36 검색 화면에 즐겨찾기 등록/취소 API 연결

* [FIX] #36 페이징 클래스에서 아이템 수정이 반영되지 않는 문제 수정

* [FEATURE] #36 링크 추가, open graph 링크 메타 정보 조회 api, datasource, repository, usecase 구현

* [FEATURE] #36 링크 추가 화면에 링크 추가, 링크 수정, 링크 메타 정보 조회 useCase 연결

* [FIX] #36 링크 추가 화면에서 링크 입력 도중 1초 이상 시간 소요시 키보드를 강제 종료시키는 문제 수정

* [CHORE] #36 ktlint 적용

* [FEATURE] #36 미분류 카테고리 컨텐츠 조회 API, datasource, api 구현

* [FIX] #36 rootNavHost에 링크 추가/삭제 화면 인자 변경 반영
  • Loading branch information
l5x5l authored Aug 16, 2024
1 parent 7c8a411 commit e58e42f
Show file tree
Hide file tree
Showing 74 changed files with 2,288 additions and 289 deletions.
4 changes: 1 addition & 3 deletions app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,9 @@ fun RootNavHost(
composable(
route = AddLink.routeWithArgs,
arguments = AddLink.arguments
) { navBackStackEntry ->
) {
val viewModel: AddLinkViewModel = hiltViewModel()
val linkId = navBackStackEntry.arguments?.getString(AddLink.linkIdArg)
AddLinkScreenContainer(
linkId = linkId,
viewModel = viewModel,
onBackPressed = navHostController::popBackStack,
onNavigateToAddPokit = {
Expand Down
8 changes: 8 additions & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

// room
implementation(libs.room.runtime)
annotationProcessor(libs.room.compiler)
kapt(libs.room.compiler)

// kotest
testImplementation(libs.kotest.runner.junit5)
testImplementation(libs.kotlin.reflect)
Expand All @@ -68,6 +73,9 @@ dependencies {

implementation(project(":domain"))

// jsoup
implementation(libs.jsoup)

// mockk
testImplementation(libs.mockk)
androidTestImplementation(libs.mockk.android)
Expand Down
55 changes: 55 additions & 0 deletions data/src/main/java/pokitmons/pokit/data/api/LinkApi.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package pokitmons.pokit.data.api

import pokitmons.pokit.data.model.link.request.ModifyLinkRequest
import pokitmons.pokit.data.model.link.response.ApplyBookmarkResponse
import pokitmons.pokit.data.model.link.response.GetLinkResponse
import pokitmons.pokit.data.model.link.response.GetLinksResponse
import pokitmons.pokit.data.model.link.response.ModifyLinkResponse
import pokitmons.pokit.domain.model.link.LinksSort
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query

Expand All @@ -19,4 +27,51 @@ interface LinkApi {
@Query("endDate") endDate: String? = null,
@Query("categoryIds") categoryIds: List<Int>? = null,
): GetLinksResponse

@GET("content")
suspend fun searchLinks(
@Query("page") page: Int = 0,
@Query("size") size: Int = 10,
@Query("sort") sort: List<String> = listOf(LinksSort.RECENT.value),
@Query("isRead") isRead: Boolean = false,
@Query("favorites") favorites: Boolean = false,
@Query("startDate") startDate: String? = null,
@Query("endDate") endDate: String? = null,
@Query("categoryIds") categoryIds: List<Int>? = null,
@Query("searchWord") searchWord: String = "",
): GetLinksResponse

@PUT("content/{contentId}")
suspend fun deleteLink(
@Path("contentId") contentId: Int = 0,
)

@POST("content/{contentId}")
suspend fun getLink(
@Path("contentId") contentId: Int = 0,
): GetLinkResponse

@PATCH("content/{contentId}")
suspend fun modifyLink(
@Path("contentId") contentId: Int,
@Body modifyLinkRequest: ModifyLinkRequest,
): ModifyLinkResponse

@POST("content")
suspend fun createLink(
@Body createLinkRequest: ModifyLinkRequest,
): ModifyLinkResponse

@PUT("content/{contentId}/bookmark")
suspend fun cancelBookmark(@Path("contentId") contentId: Int)

@POST("content/{contentId}/bookmark")
suspend fun applyBookmark(@Path("contentId") contentId: Int): ApplyBookmarkResponse

@GET("content/uncategorized")
suspend fun getUncategorizedLinks(
@Query("page") page: Int = 0,
@Query("size") size: Int = 10,
@Query("sort") sort: List<String> = listOf(LinksSort.RECENT.value),
): GetLinksResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package pokitmons.pokit.data.datasource.local.search

import android.content.SharedPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import pokitmons.pokit.data.room.dao.SearchWordDao
import pokitmons.pokit.data.room.entity.SearchWord
import java.util.Calendar
import javax.inject.Inject

class LocalSearchWordDataSource @Inject constructor(
private val searchWordDao: SearchWordDao,
private val sharedPreferences: SharedPreferences,
) : SearchDataSource {
companion object {
const val USE_RECENT_WORD_SP_KEY = "use_recent_word"
}

private val useRecentSearchWords = MutableStateFlow(
sharedPreferences.getBoolean(USE_RECENT_WORD_SP_KEY, false)
)

override fun getSearchWord(): Flow<List<String>> {
return searchWordDao.getRecentSearchWords()
}

override suspend fun addSearchWord(searchWord: String) {
val currentDateString = Calendar.getInstance()
val searchWordEntity = SearchWord(
word = searchWord,
searchedAt = currentDateString.timeInMillis.toString()
)
searchWordDao.addSearchWord(searchWord = searchWordEntity)
}

override suspend fun removeSearchWord(searchWord: String) {
searchWordDao.removeSearchWord(searchWord)
}

override suspend fun removeAllSearchWords() {
searchWordDao.removeAllSearchWords()
}

override suspend fun setUseRecentSearchWord(use: Boolean): Boolean {
sharedPreferences.edit().putBoolean(USE_RECENT_WORD_SP_KEY, use).apply()
useRecentSearchWords.update { use }
return use
}

override fun getUseRecentSearchWord(): Flow<Boolean> {
return useRecentSearchWords
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pokitmons.pokit.data.datasource.local.search

import kotlinx.coroutines.flow.Flow

interface SearchDataSource {
fun getSearchWord(): Flow<List<String>>
suspend fun addSearchWord(searchWord: String)
suspend fun removeSearchWord(searchWord: String)
suspend fun removeAllSearchWords()
suspend fun setUseRecentSearchWord(use: Boolean): Boolean
fun getUseRecentSearchWord(): Flow<Boolean>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package pokitmons.pokit.data.datasource.remote.link

import pokitmons.pokit.data.model.link.request.ModifyLinkRequest
import pokitmons.pokit.data.model.link.response.GetLinkResponse
import pokitmons.pokit.data.model.link.response.GetLinksResponse
import pokitmons.pokit.data.model.link.response.LinkCardResponse
import pokitmons.pokit.data.model.link.response.ModifyLinkResponse
import pokitmons.pokit.domain.model.link.LinksSort

interface LinkDataSource {
Expand All @@ -15,4 +19,39 @@ interface LinkDataSource {
endDate: String? = null,
categoryIds: List<Int>? = null,
): GetLinksResponse

suspend fun searchLinks(
page: Int = 0,
size: Int = 10,
sort: List<String> = listOf(LinksSort.RECENT.value),
isRead: Boolean = false,
favorites: Boolean = false,
startDate: String? = null,
endDate: String? = null,
categoryIds: List<Int>? = null,
searchWord: String = "",
): GetLinksResponse

suspend fun deleteLink(contentId: Int)

suspend fun getLink(contentId: Int): GetLinkResponse

suspend fun modifyLink(
contentId: Int,
modifyLinkRequest: ModifyLinkRequest,
): ModifyLinkResponse

suspend fun createLink(
createLinkRequest: ModifyLinkRequest,
): ModifyLinkResponse

suspend fun setBookmark(contentId: Int, bookmarked: Boolean)

suspend fun getLinkCard(url: String): LinkCardResponse

suspend fun getUncategorizedLinks(
page: Int = 0,
size: Int = 10,
sort: List<String> = listOf(LinksSort.RECENT.value),
): GetLinksResponse
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package pokitmons.pokit.data.datasource.remote.link

import org.jsoup.Jsoup
import pokitmons.pokit.data.api.LinkApi
import pokitmons.pokit.data.model.link.request.ModifyLinkRequest
import pokitmons.pokit.data.model.link.response.GetLinkResponse
import pokitmons.pokit.data.model.link.response.GetLinksResponse
import pokitmons.pokit.data.model.link.response.LinkCardResponse
import pokitmons.pokit.data.model.link.response.ModifyLinkResponse
import javax.inject.Inject

class RemoteLinkDataSource @Inject constructor(
Expand Down Expand Up @@ -30,4 +35,75 @@ class RemoteLinkDataSource @Inject constructor(
categoryIds = categoryIds
)
}

override suspend fun searchLinks(
page: Int,
size: Int,
sort: List<String>,
isRead: Boolean,
favorites: Boolean,
startDate: String?,
endDate: String?,
categoryIds: List<Int>?,
searchWord: String,
): GetLinksResponse {
return linkApi.searchLinks(
page = page,
size = size,
sort = sort,
isRead = isRead,
favorites = favorites,
startDate = startDate,
endDate = endDate,
categoryIds = categoryIds,
searchWord = searchWord
)
}

override suspend fun deleteLink(contentId: Int) {
return linkApi.deleteLink(contentId = contentId)
}

override suspend fun getLink(contentId: Int): GetLinkResponse {
return linkApi.getLink(contentId)
}

override suspend fun modifyLink(
contentId: Int,
modifyLinkRequest: ModifyLinkRequest,
): ModifyLinkResponse {
return linkApi.modifyLink(
contentId = contentId,
modifyLinkRequest = modifyLinkRequest
)
}

override suspend fun createLink(createLinkRequest: ModifyLinkRequest): ModifyLinkResponse {
return linkApi.createLink(
createLinkRequest = createLinkRequest
)
}

override suspend fun setBookmark(contentId: Int, bookmarked: Boolean) {
if (bookmarked) {
linkApi.applyBookmark(contentId)
} else {
linkApi.cancelBookmark(contentId)
}
}

override suspend fun getLinkCard(url: String): LinkCardResponse {
val document = Jsoup.connect(url).get()
val image = document.select("meta[property=og:image]").attr("content").ifEmpty { null }
val title = document.select("meta[property=og:title]").attr("content")
return LinkCardResponse(
url = url,
image = image,
title = title
)
}

override suspend fun getUncategorizedLinks(page: Int, size: Int, sort: List<String>): GetLinksResponse {
return linkApi.getUncategorizedLinks(page = page, size = size, sort = sort)
}
}
25 changes: 25 additions & 0 deletions data/src/main/java/pokitmons/pokit/data/di/core/room/RoomModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package pokitmons.pokit.data.di.core.room

import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import pokitmons.pokit.data.room.database.AppDatabase
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object RoomModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "pokitDatabase.db").build()
}

@Provides
@Singleton
fun providerSearchWordDao(database: AppDatabase) = database.searchWordDao()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pokitmons.pokit.data.di.core.sharedpreferences

import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object SharedPreferencesModule {
@Provides
@Singleton
fun provideSharedPreferences(
@ApplicationContext context: Context,
): SharedPreferences {
return context.getSharedPreferences("pokit_shared_preferences", MODE_PRIVATE)
}
}
23 changes: 23 additions & 0 deletions data/src/main/java/pokitmons/pokit/data/di/search/SearchModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pokitmons.pokit.data.di.search

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import pokitmons.pokit.data.datasource.local.search.LocalSearchWordDataSource
import pokitmons.pokit.data.datasource.local.search.SearchDataSource
import pokitmons.pokit.data.repository.search.SearchRepositoryImpl
import pokitmons.pokit.domain.repository.search.SearchRepository
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class SearchModule {
@Binds
@Singleton
abstract fun bindSearchRepository(searchRepositoryImpl: SearchRepositoryImpl): SearchRepository

@Binds
@Singleton
abstract fun bindSearchDataSource(searchDataSourceImpl: LocalSearchWordDataSource): SearchDataSource
}
Loading

0 comments on commit e58e42f

Please sign in to comment.