diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7b7faee5..50c1535c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -78,6 +78,7 @@ dependencies { implementation(project(":feature:pokitdetail")) implementation(project(":feature:search")) implementation(project(":feature:settings")) + implementation(project(":feature:home")) // hilt implementation(libs.hilt) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 352408fa..4f0287c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ + + diff --git a/core/ui/src/main/res/drawable/icon_24_remind.xml b/core/ui/src/main/res/drawable/icon_24_remind.xml new file mode 100644 index 00000000..7d6f06bf --- /dev/null +++ b/core/ui/src/main/res/drawable/icon_24_remind.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/image_floating.xml b/core/ui/src/main/res/drawable/image_floating.xml new file mode 100644 index 00000000..f1183b4b --- /dev/null +++ b/core/ui/src/main/res/drawable/image_floating.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/ui/src/main/res/drawable/logo_pokit.xml b/core/ui/src/main/res/drawable/logo_pokit.xml new file mode 100644 index 00000000..fbdaba70 --- /dev/null +++ b/core/ui/src/main/res/drawable/logo_pokit.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/logo_remind.xml b/core/ui/src/main/res/drawable/logo_remind.xml new file mode 100644 index 00000000..25bcb6f7 --- /dev/null +++ b/core/ui/src/main/res/drawable/logo_remind.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/src/main/java/pokitmons/pokit/data/api/RemindApi.kt b/data/src/main/java/pokitmons/pokit/data/api/RemindApi.kt new file mode 100644 index 00000000..b912b7f8 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/api/RemindApi.kt @@ -0,0 +1,30 @@ +package pokitmons.pokit.data.api + +import pokitmons.pokit.data.model.home.remind.Remind +import pokitmons.pokit.data.model.home.remind.RemindResponse +import pokitmons.pokit.domain.model.pokit.PokitsSort +import retrofit2.http.GET +import retrofit2.http.Query + +interface RemindApi { + @GET("remind/unread") + suspend fun getUnreadContents( + @Query("size") size: Int = 10, + @Query("page") page: Int = 0, + @Query("sort") sort: String = PokitsSort.RECENT.value, + ): RemindResponse + + @GET("remind/today") + suspend fun getTodayContents( + @Query("size") size: Int = 10, + @Query("page") page: Int = 0, + @Query("sort") sort: String = PokitsSort.RECENT.value, + ): List + + @GET("remind/bookmark") + suspend fun getBookmarkContents( + @Query("size") size: Int = 10, + @Query("page") page: Int = 0, + @Query("sort") sort: String = PokitsSort.RECENT.value, + ): RemindResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSource.kt new file mode 100644 index 00000000..dbf1ee25 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSource.kt @@ -0,0 +1,11 @@ +package pokitmons.pokit.data.datasource.remote.home.remind + +import pokitmons.pokit.data.model.home.remind.Remind +import pokitmons.pokit.data.model.home.remind.RemindRequest +import pokitmons.pokit.data.model.home.remind.RemindResponse + +interface RemindDataSource { + suspend fun getUnreadContents(remindRequest: RemindRequest): RemindResponse + suspend fun getTodayContents(remindRequest: RemindRequest): List + suspend fun getBookmarkContents(remindRequest: RemindRequest): RemindResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSourceImpl.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSourceImpl.kt new file mode 100644 index 00000000..0b88996f --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSourceImpl.kt @@ -0,0 +1,21 @@ +package pokitmons.pokit.data.datasource.remote.home.remind + +import pokitmons.pokit.data.api.RemindApi +import pokitmons.pokit.data.model.home.remind.Remind +import pokitmons.pokit.data.model.home.remind.RemindRequest +import pokitmons.pokit.data.model.home.remind.RemindResponse +import javax.inject.Inject + +class RemindDataSourceImpl @Inject constructor(private val remindApi: RemindApi) : RemindDataSource { + override suspend fun getUnreadContents(remindRequest: RemindRequest): RemindResponse { + return remindApi.getUnreadContents() + } + + override suspend fun getTodayContents(remindRequest: RemindRequest): List { + return remindApi.getTodayContents() + } + + override suspend fun getBookmarkContents(remindRequest: RemindRequest): RemindResponse { + return remindApi.getBookmarkContents() + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/home/remind/RemindModule.kt b/data/src/main/java/pokitmons/pokit/data/di/home/remind/RemindModule.kt new file mode 100644 index 00000000..3905e080 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/home/remind/RemindModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.home.remind + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.datasource.remote.home.remind.RemindDataSource +import pokitmons.pokit.data.datasource.remote.home.remind.RemindDataSourceImpl +import pokitmons.pokit.data.repository.home.remind.RemindRepositoryImpl +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RemindModule { + @Binds + @Singleton + abstract fun bindRemindRepository(remindRepositoryImpl: RemindRepositoryImpl): RemindRepository + + @Binds + @Singleton + abstract fun bindRemindDataSource(remindDataSourceImpl: RemindDataSourceImpl): RemindDataSource +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/network/BearerTokenInterceptor.kt b/data/src/main/java/pokitmons/pokit/data/di/network/BearerTokenInterceptor.kt index 769fc857..1a5fe4ed 100644 --- a/data/src/main/java/pokitmons/pokit/data/di/network/BearerTokenInterceptor.kt +++ b/data/src/main/java/pokitmons/pokit/data/di/network/BearerTokenInterceptor.kt @@ -13,8 +13,8 @@ class BearerTokenInterceptor : Interceptor { val requestWithToken: Request = originalRequest.newBuilder() .header( "Authorization", - "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIxIiwiaWF0IjoxNzIxNjU4MjUxLCJleHAiOj" + - "MwMDE3MjE2NTgyNTF9.gw6LZimKLuZJ2y0UV5cgvk3F7o92pkRIDgx-qlD_S7qEI01QAFt9dZDyHADabftI" + "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIxMCIsImlhdCI6MTcyMzY0MzEzOSwiZXhwIjoy" + + "MDIzNjQzMTM5fQ.3jJ6rpPCaMKSrmiB3NtQ3_sYH0zbBuoS0GAwX69HCu62-Vk6x--eUu4dhZJTmqlm" ) .build() diff --git a/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt b/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt index f03ca042..8485c009 100644 --- a/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt +++ b/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt @@ -12,6 +12,7 @@ import okhttp3.logging.HttpLoggingInterceptor import pokitmons.pokit.data.api.AuthApi import pokitmons.pokit.data.api.LinkApi import pokitmons.pokit.data.api.PokitApi +import pokitmons.pokit.data.api.RemindApi import pokitmons.pokit.data.api.SettingApi import retrofit2.Retrofit import java.util.concurrent.TimeUnit @@ -85,4 +86,9 @@ object NetworkModule { fun provideSettingService(retrofit: Retrofit): SettingApi { return retrofit.create(SettingApi::class.java) } + + @Provides + fun provideRemindService(retrofit: Retrofit): RemindApi { + return retrofit.create(RemindApi::class.java) + } } diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/home/home/RemindMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/home/home/RemindMapper.kt new file mode 100644 index 00000000..f95a100d --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/mapper/home/home/RemindMapper.kt @@ -0,0 +1,33 @@ +package pokitmons.pokit.data.mapper.home.home + +import pokitmons.pokit.data.model.home.remind.Remind +import pokitmons.pokit.data.model.home.remind.RemindResponse +import pokitmons.pokit.domain.model.home.remind.RemindResult + +object RemindMapper { + fun mapperToRemind(remindResponse: RemindResponse): List { + return remindResponse.data.map { remind -> + RemindResult( + title = remind.title, + domain = remind.domain, + createdAt = remind.createdAt, + isRead = remind.isRead, + thumbNail = remind.thumbNail, + data = remind.data + ) + } + } + + fun mapperToTodayContents(remindResponse: List): List { + return remindResponse.map { remind -> + RemindResult( + title = remind.title, + domain = remind.domain, + createdAt = remind.createdAt, + isRead = remind.isRead, + thumbNail = remind.thumbNail, + data = remind.data + ) + } + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/pokit/PokitMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/pokit/PokitMapper.kt index f87073e0..fdeddcbd 100644 --- a/data/src/main/java/pokitmons/pokit/data/mapper/pokit/PokitMapper.kt +++ b/data/src/main/java/pokitmons/pokit/data/mapper/pokit/PokitMapper.kt @@ -18,6 +18,7 @@ object PokitMapper { id = data.categoryImage.imageId, url = data.categoryImage.imageUrl ), + createdAt = data.createdAt, linkCount = data.contentCount ) } @@ -38,7 +39,8 @@ object PokitMapper { id = pokitResponse.categoryImage.imageId, url = pokitResponse.categoryImage.imageUrl ), - linkCount = NOT_USE + linkCount = NOT_USE, + createdAt = pokitResponse.createdAt ) } } diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/Category.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Category.kt new file mode 100644 index 00000000..5c119e7b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Category.kt @@ -0,0 +1,9 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable + +@Serializable +data class Category( + val categoryId: Int, + val categoryName: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/Remind.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Remind.kt new file mode 100644 index 00000000..21cc45a3 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Remind.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable + +@Serializable +data class Remind( + val category: Category, + val contentId: Int, + val createdAt: String, + val data: String, + val domain: String, + val isRead: Boolean, + val thumbNail: String, + val title: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindRequest.kt new file mode 100644 index 00000000..6aec9476 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindRequest.kt @@ -0,0 +1,11 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable +import pokitmons.pokit.domain.model.pokit.PokitsSort + +@Serializable +data class RemindRequest( + val size: Int = 10, + val page: Int = 0, + val sort: PokitsSort = PokitsSort.RECENT, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindResponse.kt new file mode 100644 index 00000000..d2789fbf --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindResponse.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable + +@Serializable +data class RemindResponse( + val data: List = emptyList(), + val hasNext: Boolean = false, + val page: Int = 0, + val size: Int = 10, + val sort: List = emptyList(), +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/Sort.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Sort.kt new file mode 100644 index 00000000..48e4f07b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Sort.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable + +@Serializable +data class Sort( + val ascending: Boolean, + val direction: String, + val ignoreCase: Boolean, + val nullHandling: String, + val property: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitResponse.kt index 65136c7c..2ea6ccb3 100644 --- a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitResponse.kt +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitResponse.kt @@ -7,6 +7,7 @@ data class GetPokitResponse( val categoryId: Int = 0, val categoryName: String = "", val categoryImage: Image = Image(), + val createdAt: String = "", ) { @Serializable data class Image( diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitsResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitsResponse.kt index 6cdaff9b..87790c1b 100644 --- a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitsResponse.kt +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitsResponse.kt @@ -17,6 +17,7 @@ data class GetPokitsResponse( val categoryName: String, val categoryImage: PokitImage, val contentCount: Int, + val createdAt: String, ) @Serializable diff --git a/data/src/main/java/pokitmons/pokit/data/repository/home/remind/RemindRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/home/remind/RemindRepositoryImpl.kt new file mode 100644 index 00000000..5cf6ddce --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/repository/home/remind/RemindRepositoryImpl.kt @@ -0,0 +1,58 @@ +package pokitmons.pokit.data.repository.home.remind + +import pokitmons.pokit.data.datasource.remote.home.remind.RemindDataSource +import pokitmons.pokit.data.mapper.home.home.RemindMapper +import pokitmons.pokit.data.model.common.parseErrorResult +import pokitmons.pokit.data.model.home.remind.RemindRequest +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.model.pokit.PokitsSort +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Inject + +class RemindRepositoryImpl @Inject constructor(private val remindDataSource: RemindDataSource) : RemindRepository { + override suspend fun getUnReadContents( + filterUncategorized: Boolean, + size: Int, + page: Int, + sort: PokitsSort, + ): PokitResult> { + return runCatching { + val response = remindDataSource.getUnreadContents(RemindRequest()) + val remindResponse = RemindMapper.mapperToRemind(response) + PokitResult.Success(remindResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getTodayContents( + filterUncategorized: Boolean, + size: Int, + page: Int, + sort: PokitsSort, + ): PokitResult> { + return runCatching { + val response = remindDataSource.getTodayContents(RemindRequest()) + val remindResponse = RemindMapper.mapperToTodayContents(response) + PokitResult.Success(remindResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getBookmarkContents( + filterUncategorized: Boolean, + size: Int, + page: Int, + sort: PokitsSort, + ): PokitResult> { + return runCatching { + val response = remindDataSource.getBookmarkContents(RemindRequest()) + val remindResponse = RemindMapper.mapperToRemind(response) + PokitResult.Success(remindResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/home/remind/RemindResult.kt b/domain/src/main/java/pokitmons/pokit/domain/model/home/remind/RemindResult.kt new file mode 100644 index 00000000..6518a021 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/home/remind/RemindResult.kt @@ -0,0 +1,10 @@ +package pokitmons.pokit.domain.model.home.remind + +data class RemindResult( + val title: String, + val domain: String, + val createdAt: String, + val isRead: Boolean, + val thumbNail: String, + val data: String, +) diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Pokit.kt b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Pokit.kt index fe4ba3e8..06a376f0 100644 --- a/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Pokit.kt +++ b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Pokit.kt @@ -6,6 +6,7 @@ data class Pokit( val name: String, val image: Image, val linkCount: Int, + val createdAt: String, ) { data class Image( val id: Int, diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/home/remind/RemindRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/home/remind/RemindRepository.kt new file mode 100644 index 00000000..d86b89f9 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/home/remind/RemindRepository.kt @@ -0,0 +1,28 @@ +package pokitmons.pokit.domain.repository.home.remind + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.model.pokit.PokitsSort + +interface RemindRepository { + suspend fun getUnReadContents( + filterUncategorized: Boolean = true, + size: Int = 10, + page: Int = 0, + sort: PokitsSort = PokitsSort.RECENT, + ): PokitResult> + + suspend fun getTodayContents( + filterUncategorized: Boolean = true, + size: Int = 10, + page: Int = 0, + sort: PokitsSort = PokitsSort.RECENT, + ): PokitResult> + + suspend fun getBookmarkContents( + filterUncategorized: Boolean = true, + size: Int = 10, + page: Int = 0, + sort: PokitsSort = PokitsSort.RECENT, + ): PokitResult> +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/BookMarkContentsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/BookMarkContentsUseCase.kt new file mode 100644 index 00000000..4d589dc8 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/BookMarkContentsUseCase.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.usecase.home.remind + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Inject + +class BookMarkContentsUseCase @Inject constructor(private val remindRepository: RemindRepository) { + suspend fun getBookmarkContents(): PokitResult> { + return remindRepository.getBookmarkContents() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/TodayContentsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/TodayContentsUseCase.kt new file mode 100644 index 00000000..6a4cf317 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/TodayContentsUseCase.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.usecase.home.remind + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Inject + +class TodayContentsUseCase @Inject constructor(private val remindRepository: RemindRepository) { + suspend fun getTodayContents(): PokitResult> { + return remindRepository.getTodayContents() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/UnReadContentsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/UnReadContentsUseCase.kt new file mode 100644 index 00000000..5b449479 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/UnReadContentsUseCase.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.usecase.home.remind + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Inject + +class UnReadContentsUseCase @Inject constructor(private val remindRepository: RemindRepository) { + suspend fun getUnreadContents(): PokitResult> { + return remindRepository.getUnReadContents() + } +} diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts new file mode 100644 index 00000000..4e02a173 --- /dev/null +++ b/feature/home/build.gradle.kts @@ -0,0 +1,77 @@ +plugins { + alias(libs.plugins.com.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") +} + +android { + namespace = "pokitmons.pokit.home" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + + // navigation + implementation(libs.androidx.navigation.compose) + implementation(libs.hilt.navigation.compose) + + // coil + implementation(libs.coil.compose) + + // module + implementation(project(":core:ui")) + implementation(project(":domain")) + implementation(project(":feature:pokitdetail")) +} diff --git a/feature/home/proguard-rules.pro b/feature/home/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/home/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/home/src/androidTest/java/pokitmons/pokit/home/ExampleInstrumentedTest.kt b/feature/home/src/androidTest/java/pokitmons/pokit/home/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..ccdaae64 --- /dev/null +++ b/feature/home/src/androidTest/java/pokitmons/pokit/home/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package pokitmons.pokit.home + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("pokitmons.pokit.home", appContext.packageName) + } +} diff --git a/feature/home/src/main/AndroidManifest.xml b/feature/home/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/home/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/home/src/main/java/pokitmons/pokit/home/BottomNavigationBar.kt b/feature/home/src/main/java/pokitmons/pokit/home/BottomNavigationBar.kt new file mode 100644 index 00000000..565ac23e --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/BottomNavigationBar.kt @@ -0,0 +1,106 @@ +package pokitmons.pokit.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.material3.BottomAppBar +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.home.pokit.PokitViewModel +import pokitmons.pokit.home.pokit.ScreenType +import pokitmons.pokit.core.ui.R.drawable as DrawableResource + +// TODO : 바텀시트 아이템 컴포저블로 만들기 + +@Composable +fun BottomNavigationBar(viewModel: PokitViewModel = hiltViewModel()) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + shadowElevation = 20.dp + ) { + BottomAppBar( + containerColor = PokitTheme.colors.backgroundBase, + modifier = Modifier.height(92.dp), + tonalElevation = 8.dp + ) { + Column( + modifier = Modifier + .weight(2f) + .padding(bottom = 24.dp) + .clickable { viewModel.updateScreenType(ScreenType.Pokit) }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = DrawableResource.icon_24_folder), + contentDescription = "리마인드", + tint = when (viewModel.screenType.value) { + is ScreenType.Pokit -> Color.Black + is ScreenType.Remind -> PokitTheme.colors.iconTertiary + }, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + color = when (viewModel.screenType.value) { + is ScreenType.Pokit -> Color.Black + is ScreenType.Remind -> PokitTheme.colors.textTertiary + }, + style = PokitTheme.typography.detail2, + text = "포킷", + textAlign = TextAlign.Center + ) + } + + Column( + modifier = Modifier + .weight(2f) + .padding(bottom = 24.dp) + .clickable { viewModel.updateScreenType(ScreenType.Remind) }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = DrawableResource.icon_24_remind), + contentDescription = "리마인드", + tint = when (viewModel.screenType.value) { + is ScreenType.Remind -> Color.Black + is ScreenType.Pokit -> PokitTheme.colors.iconTertiary + }, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + color = when (viewModel.screenType.value) { + is ScreenType.Remind -> Color.Black + is ScreenType.Pokit -> PokitTheme.colors.textTertiary + }, + style = PokitTheme.typography.detail2, + text = "리마인드", + textAlign = TextAlign.Center + ) + } + } + } +} + +@Preview +@Composable +fun BottomNavigationBarPreview() { + BottomNavigationBar() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/HomeHeader.kt b/feature/home/src/main/java/pokitmons/pokit/home/HomeHeader.kt new file mode 100644 index 00000000..92d663a5 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/HomeHeader.kt @@ -0,0 +1,80 @@ +package pokitmons.pokit.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material3.Icon +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.res.painterResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.home.pokit.PokitViewModel +import pokitmons.pokit.home.pokit.ScreenType + +@Composable +fun HomeHeader( + viewModel: PokitViewModel, + onNavigateToSetting: () -> Unit, + onNavigateToSearch: () -> Unit, +) { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .background(color = Color.White) + .fillMaxWidth() + .height(56.dp) + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = when (viewModel.screenType.value) { + is ScreenType.Pokit -> painterResource(id = R.drawable.logo_pokit) + is ScreenType.Remind -> painterResource(id = R.drawable.logo_remind) + }, + tint = PokitTheme.colors.brand, + contentDescription = "로고" + ) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + painterResource(id = R.drawable.icon_24_search), + contentDescription = "검색", + modifier = Modifier + .size(24.dp) + .clickable { onNavigateToSearch() } + ) + Icon( + painterResource(id = R.drawable.icon_24_bell), + contentDescription = "알림", + modifier = Modifier.size(24.dp) + ) + + when (viewModel.screenType.value) { + is ScreenType.Pokit -> { + Icon( + painterResource(id = R.drawable.icon_24_setup), + contentDescription = "설정", + modifier = Modifier + .size(24.dp) + .clickable { onNavigateToSetting() } + ) + } + is ScreenType.Remind -> Unit + } + } + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt new file mode 100644 index 00000000..7ab6133b --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt @@ -0,0 +1,183 @@ +package pokitmons.pokit.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.home.pokit.PokitScreen +import pokitmons.pokit.home.pokit.PokitViewModel +import pokitmons.pokit.home.pokit.ScreenType +import pokitmons.pokit.home.remind.RemindScreen + +// TODO 화면 단으로 분라 +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen( + viewModel: PokitViewModel, + onNavigateToPokitDetail: (String) -> Unit, + onNavigateToSearch: () -> Unit, + onNavigateToSetting: () -> Unit, + onNavigateAddLink: () -> Unit, + onNavigateAddPokit: () -> Unit, + +) { + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + var showBottomSheet by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .background(color = Color.White) + .fillMaxSize() + ) { + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, + sheetState = sheetState + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(180.dp), + horizontalArrangement = Arrangement.Center + ) { + Column( + modifier = Modifier + .background( + color = PokitTheme.colors.brand, + shape = RoundedCornerShape(12.dp) + ) + .size(96.dp) + .clickable { + scope.launch { + sheetState.hide() + showBottomSheet = false + onNavigateAddLink() + } + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + tint = PokitTheme.colors.inverseWh, + modifier = Modifier.size(32.dp), + painter = painterResource(id = R.drawable.icon_24_link), + contentDescription = null + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + style = PokitTheme.typography.body3Medium, + text = "링크추가", + color = PokitTheme.colors.inverseWh + ) + } + + Spacer(modifier = Modifier.padding(horizontal = 10.dp)) + + Column( + modifier = Modifier + .background( + color = PokitTheme.colors.brand, + shape = RoundedCornerShape(12.dp) + ) + .size(96.dp) + .clickable { + scope.launch { + sheetState.hide() + showBottomSheet = false + onNavigateAddPokit() + } + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + tint = PokitTheme.colors.inverseWh, + modifier = Modifier.size(32.dp), + painter = painterResource(id = R.drawable.icon_24_folder), + contentDescription = null + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + style = PokitTheme.typography.body3Medium, + text = "포킷추가", + color = PokitTheme.colors.inverseWh + ) + } + } + } + } + + // screen + Column( + modifier = Modifier + .background(color = Color.White) + .fillMaxSize() + ) { + HomeHeader( + viewModel = viewModel, + onNavigateToSearch = { onNavigateToSearch() }, + onNavigateToSetting = { onNavigateToSetting() } + ) + Scaffold( + bottomBar = { BottomNavigationBar() } + ) { padding -> + when (viewModel.screenType.value) { + is ScreenType.Pokit -> { + PokitScreen( + viewModel = viewModel, + modifier = Modifier.padding(padding), + onNavigateToPokitDetail = onNavigateToPokitDetail + ) + } + + is ScreenType.Remind -> { + RemindScreen(Modifier.padding(padding)) + } + } + } + } + Image( + modifier = Modifier + .align(Alignment.BottomCenter) + .clickable { + showBottomSheet = true + } + .padding(bottom = 60.dp), + painter = painterResource(id = R.drawable.image_floating), + contentDescription = null + ) + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/CategorySelector.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/CategorySelector.kt new file mode 100644 index 00000000..6947277e --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/CategorySelector.kt @@ -0,0 +1,116 @@ +package pokitmons.pokit.home.pokit + +import androidx.compose.foundation.clickable +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import pokitmons.pokit.core.ui.components.atom.button.PokitButton +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIcon +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIconPosition +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonShape +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonStyle +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonType +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.R.drawable as DrawableResource + +@Composable +fun HomeMid(viewModel: PokitViewModel = hiltViewModel()) { + Spacer(modifier = Modifier.height(24.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + PokitButton( + style = when (viewModel.selectedCategory.value) { + is Category.Pokit -> PokitButtonStyle.FILLED + is Category.Unclassified -> PokitButtonStyle.STROKE + }, + text = "포킷", + shape = PokitButtonShape.ROUND, + icon = PokitButtonIcon( + DrawableResource.icon_24_folderline, + PokitButtonIconPosition.LEFT + ), + type = when (viewModel.selectedCategory.value) { + is Category.Pokit -> PokitButtonType.PRIMARY + is Category.Unclassified -> PokitButtonType.SECONDARY + }, + onClick = { viewModel.updateCategory(Category.Pokit) } + ) + + Spacer(modifier = Modifier.padding(start = 12.dp)) + + PokitButton( + style = when (viewModel.selectedCategory.value) { + is Category.Pokit -> PokitButtonStyle.STROKE + is Category.Unclassified -> PokitButtonStyle.FILLED + }, + text = "미분류", + shape = PokitButtonShape.ROUND, + icon = PokitButtonIcon( + DrawableResource.icon_24_info, + PokitButtonIconPosition.LEFT + ), + type = when (viewModel.selectedCategory.value) { + is Category.Pokit -> PokitButtonType.SECONDARY + is Category.Unclassified -> PokitButtonType.PRIMARY + }, + onClick = { viewModel.updateCategory(Category.Unclassified) } + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Icon( + modifier = Modifier.size(18.dp), + painter = painterResource(id = DrawableResource.icon_24_align), + contentDescription = null + ) + + Spacer(modifier = Modifier.padding(start = 2.dp)) + + Text( + modifier = Modifier + .clickable { + when (viewModel.sortOrder.value) { + is SortOrder.Latest -> viewModel.updateSortOrder(SortOrder.Name) + is SortOrder.Name -> viewModel.updateSortOrder(SortOrder.Latest) + } + } + .align(Alignment.CenterVertically), + text = when (viewModel.sortOrder.value) { + is SortOrder.Latest -> "최신순" + is SortOrder.Name -> "이름순" + }, + style = PokitTheme.typography.body3Medium + ) + } + } + Spacer(modifier = Modifier.height(20.dp)) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewCustomRow() { + HomeMid() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt new file mode 100644 index 00000000..d55045ae --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt @@ -0,0 +1,85 @@ +package pokitmons.pokit.home.pokit + +import androidx.compose.foundation.background +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.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.core.ui.components.block.pokitcard.PokitCard +import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet +import pokitmons.pokit.core.ui.components.template.modifybottomsheet.ModifyBottomSheetContent + +@Composable +fun PokitScreen( + modifier: Modifier = Modifier, + viewModel: PokitViewModel, + onNavigateToPokitDetail: (String) -> Unit, +) { + viewModel.loadPokits() + var showBottomSheet by remember { mutableStateOf(false) } + val pokits = viewModel.pokits.collectAsState() + + Column( + modifier = Modifier + .padding(horizontal = 20.dp) + .background(color = Color.White) + .fillMaxSize() + ) { + HomeMid() + + when (viewModel.selectedCategory.value) { + is Category.Pokit -> { + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(bottom = 100.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(pokits.value) { pokitDetail -> + PokitCard( + text = pokitDetail.title, + linkCount = pokitDetail.count, + painter = rememberAsyncImagePainter(model = pokitDetail.image.url), + onClick = { onNavigateToPokitDetail(pokitDetail.id) }, + onClickKebab = { + showBottomSheet = true + } + ) + } + } + } + + is Category.Unclassified -> { + UnclassifiedScreen() + } + } + + if (showBottomSheet) { + PokitBottomSheet( + onHideBottomSheet = { showBottomSheet = false }, + show = showBottomSheet + ) { + ModifyBottomSheetContent( + onClickShare = { }, + onClickRemove = { }, + onClickModify = { } + ) + } + } + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt new file mode 100644 index 00000000..dd191d15 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt @@ -0,0 +1,127 @@ +package pokitmons.pokit.home.pokit + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.strayalpaca.pokitdetail.model.Pokit +import com.strayalpaca.pokitdetail.paging.LinkPaging +import com.strayalpaca.pokitdetail.paging.PokitPaging +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.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.Link +import pokitmons.pokit.domain.model.link.LinksSort +import pokitmons.pokit.domain.usecase.link.GetLinksUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import javax.inject.Inject +import com.strayalpaca.pokitdetail.model.Link as DetailLink + +@HiltViewModel +class PokitViewModel @Inject constructor( + private val getPokitsUseCase: GetPokitsUseCase, + private val getLinksUseCase: GetLinksUseCase, +) : ViewModel() { + + var selectedCategory = mutableStateOf(Category.Pokit) + private set + + var sortOrder = mutableStateOf(SortOrder.Latest) + private set + + var screenType = mutableStateOf(ScreenType.Pokit) + private set + + private val pokitPaging = PokitPaging( + getPokits = getPokitsUseCase, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0 + ) + + private val linkPaging = LinkPaging( + getLinks = ::getUncategorizedLinks, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0, + initCategoryId = 1 + ) + + private var _pokits: MutableStateFlow> = pokitPaging._pagingData + val pokits: StateFlow> + get() = _pokits.asStateFlow() + + private var _unCategoryLinks: MutableStateFlow> = linkPaging._pagingData + val unCategoryLinks: StateFlow> + get() = _unCategoryLinks.asStateFlow() + + fun updateCategory(category: Category) { + selectedCategory.value = category + } + + fun updateSortOrder(order: SortOrder) { + sortOrder.value = order + sortPokits() + } + + private fun sortPokits() { + when (sortOrder.value) { + is SortOrder.Name -> { + _pokits.update { pokit -> + pokit.sortedBy { pokitDetail -> + pokitDetail.title + } + } + } + is SortOrder.Latest -> { + _pokits.update { pokit -> + pokit.sortedByDescending { pokitDetail -> + pokitDetail.createdAt + } + } + } + } + } + + private suspend fun getUncategorizedLinks(categoryId: Int, size: Int, page: Int, sort: LinksSort): PokitResult> { + return getLinksUseCase.getUncategorizedLinks( + size = size, + page = page, + sort = sort + ) + } + + fun updateScreenType(type: ScreenType) { + screenType.value = type + } + + fun loadPokits() { + viewModelScope.launch { + pokitPaging.load() + } + } + + fun loadUnCategoryLinks() { + viewModelScope.launch { + linkPaging.load() + } + } +} + +sealed class Category { + data object Pokit : Category() + data object Unclassified : Category() +} + +sealed class SortOrder { + data object Latest : SortOrder() + data object Name : SortOrder() +} + +sealed class ScreenType { + data object Pokit : ScreenType() + data object Remind : ScreenType() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/UnclassifiedScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/UnclassifiedScreen.kt new file mode 100644 index 00000000..fbe6904e --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/UnclassifiedScreen.kt @@ -0,0 +1,45 @@ +package pokitmons.pokit.home.pokit + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard + +@Composable +fun UnclassifiedScreen(viewModel: PokitViewModel = hiltViewModel()) { + viewModel.loadUnCategoryLinks() + val unCategoryLinks = viewModel.unCategoryLinks.collectAsState() + + LazyColumn( + modifier = Modifier, + contentPadding = PaddingValues(bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + items(items = unCategoryLinks.value) { unCategoryDetail -> + LinkCard( + item = unCategoryDetail.linkType, + title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", + sub = unCategoryDetail.createdAt, + painter = rememberAsyncImagePainter(model = unCategoryDetail.imageUrl), + notRead = !unCategoryDetail.isRead, + badgeText = "미분류", + onClickKebab = { }, + onClickItem = { } + ) + } + } +} + +@Preview +@Composable +fun LinkCardPreview2() { + UnclassifiedScreen() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt new file mode 100644 index 00000000..e9c6e4c1 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt @@ -0,0 +1,102 @@ +package pokitmons.pokit.home.remind + +import androidx.compose.foundation.horizontalScroll +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.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard + +@Composable +fun RemindScreen( + modifier: Modifier = Modifier, + viewModel: RemindViewModel = hiltViewModel(), +) { + val unreadContents = viewModel.unReadContents.collectAsState() + val todayContents = viewModel.todayContents.collectAsState() + val bookmarkContents = viewModel.bookmarkContents.collectAsState() + + Column( + modifier = modifier + .padding(20.dp) + .fillMaxHeight() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(4.dp)) + + RemindSection(title = "오늘 이 링크는 어때요?") { + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + todayContents.value.forEach { todayContent -> + ToadyLinkCard( + title = todayContent.title, + sub = todayContent.createdAt, + painter = rememberAsyncImagePainter(todayContent.thumbNail), + badgeText = todayContent.data, + domain = todayContent.domain + ) + } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + RemindSection(title = "한번도 읽지 않았어요") { + Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + unreadContents.value.forEach { unReadContent -> + LinkCard( + item = unReadContent.title, + title = unReadContent.title, + sub = "${unReadContent.createdAt} • ${unReadContent.domain}", + painter = rememberAsyncImagePainter(unReadContent.thumbNail), + notRead = unReadContent.isRead, + badgeText = unReadContent.data, + onClickKebab = { }, + onClickItem = { } + ) + } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + RemindSection(title = "즐겨찾기 링크만 모았어요") { + Spacer(modifier = Modifier.height(12.dp)) + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + bookmarkContents.value.forEach { unReadContent -> + LinkCard( + item = unReadContent.title, + title = unReadContent.title, + sub = "${unReadContent.createdAt} • ${unReadContent.domain}", + painter = rememberAsyncImagePainter(unReadContent.thumbNail), + notRead = unReadContent.isRead, + badgeText = unReadContent.data, + onClickKebab = { }, + onClickItem = { } + ) + } + } + } + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindSection.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindSection.kt new file mode 100644 index 00000000..bb353e83 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindSection.kt @@ -0,0 +1,20 @@ +package pokitmons.pokit.home.remind + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun RemindSection( + title: String, + content: @Composable () -> Unit, +) { + Column { + Text( + text = title, + style = PokitTheme.typography.title2 + ) + content() + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt new file mode 100644 index 00000000..bf3deefb --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt @@ -0,0 +1,61 @@ +package pokitmons.pokit.home.remind + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.usecase.home.remind.BookMarkContentsUseCase +import pokitmons.pokit.domain.usecase.home.remind.TodayContentsUseCase +import pokitmons.pokit.domain.usecase.home.remind.UnReadContentsUseCase +import javax.inject.Inject + +@HiltViewModel +class RemindViewModel @Inject constructor( + private val unReadContentsUseCase: UnReadContentsUseCase, + private val todayContentsUseCase: TodayContentsUseCase, + private val bookMarkContentsUseCase: BookMarkContentsUseCase, +) : ViewModel() { + + init { + loadContents() + } + + private var _unReadContents: MutableStateFlow> = MutableStateFlow(emptyList()) + val unReadContents: StateFlow> + get() = _unReadContents.asStateFlow() + + private var _todayContents: MutableStateFlow> = MutableStateFlow(emptyList()) + val todayContents: StateFlow> + get() = _todayContents.asStateFlow() + + private var _bookmarkContents: MutableStateFlow> = MutableStateFlow(emptyList()) + val bookmarkContents: StateFlow> + get() = _bookmarkContents.asStateFlow() + + fun loadContents() { + viewModelScope.launch { + when (val response = unReadContentsUseCase.getUnreadContents()) { + is PokitResult.Success -> _unReadContents.value = response.result.take(3) + is PokitResult.Error -> {} + } + + when (val response = todayContentsUseCase.getTodayContents()) { + is PokitResult.Success -> { + _todayContents.value = response.result + } + is PokitResult.Error -> { + } + } + + when (val response = bookMarkContentsUseCase.getBookmarkContents()) { + is PokitResult.Success -> _bookmarkContents.value = response.result.take(3) + is PokitResult.Error -> {} + } + } + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/TodayLinkCard.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/TodayLinkCard.kt new file mode 100644 index 00000000..3c01a522 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/TodayLinkCard.kt @@ -0,0 +1,112 @@ +package pokitmons.pokit.home.remind + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.home.R + +@Composable +fun ToadyLinkCard( + viewModel: RemindViewModel = hiltViewModel(), + title: String, + sub: String, + painter: Painter, + badgeText: String, + domain: String, +) { + Box( + modifier = Modifier + .width(216.dp) + .height(194.dp) + .clip(RoundedCornerShape(8.dp)) + ) { + Image( + painter = painter, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf(Color.Transparent, Color(0xFF060606)) + ) + ) + ) + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(12.dp) + ) { + Text( + style = PokitTheme.typography.label4, + text = badgeText, + color = PokitTheme.colors.textTertiary, + modifier = Modifier + .height(16.dp) + .width(34.dp) + .clip(RoundedCornerShape(4.dp)) + .background(PokitTheme.colors.backgroundPrimary) + .padding(horizontal = 4.dp, vertical = 2.dp) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Text( + modifier = Modifier.weight(1f), + style = PokitTheme.typography.body2Bold, + text = title, + color = Color.White, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.padding(4.dp)) + + Icon( + painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_kebab), + contentDescription = null, + tint = Color.White + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "$sub · $domain", + style = PokitTheme.typography.detail2, + color = PokitTheme.colors.textTertiary + ) + } + } +} diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml new file mode 100644 index 00000000..19bf5d4e --- /dev/null +++ b/feature/home/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + home + \ No newline at end of file diff --git a/feature/home/src/test/java/pokitmons/pokit/home/ExampleUnitTest.kt b/feature/home/src/test/java/pokitmons/pokit/home/ExampleUnitTest.kt new file mode 100644 index 00000000..13a91fbb --- /dev/null +++ b/feature/home/src/test/java/pokitmons/pokit/home/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.home + +import junit.framework.TestCase.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Link.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Link.kt index 4f2129aa..d29f99ae 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Link.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Link.kt @@ -14,6 +14,7 @@ data class Link( val memo: String = "", val bookmark: Boolean = false, val imageUrl: String? = null, + val createdAt: String = "", ) { companion object { fun fromDomainLink(domainLink: DomainLink): Link { @@ -25,7 +26,8 @@ data class Link( isRead = domainLink.isRead, url = domainLink.data, memo = domainLink.memo, - imageUrl = domainLink.thumbnail + imageUrl = domainLink.thumbnail, + createdAt = domainLink.createdAt ) } } diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Pokit.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Pokit.kt index 1f97135e..47806ad3 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Pokit.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Pokit.kt @@ -6,22 +6,26 @@ data class Pokit( val title: String = "", val id: String = "", val count: Int = 0, + val image: pokitmons.pokit.domain.model.pokit.Pokit.Image, + val createdAt: String, ) { companion object { fun fromDomainPokit(pokit: DomainPokit): Pokit { return Pokit( title = pokit.name, id = pokit.categoryId.toString(), - count = pokit.linkCount + count = pokit.linkCount, + image = pokit.image, + createdAt = pokit.createdAt ) } } } -internal val samplePokitList = listOf( - Pokit(title = "안드로이드", id = "1", count = 2), - Pokit(title = "IOS", id = "2", count = 2), - Pokit(title = "디자인", id = "3", count = 2), - Pokit(title = "PM", id = "4", count = 1), - Pokit(title = "서버", id = "5", count = 2) -) +// internal val samplePokitList = listOf( +// Pokit(title = "안드로이드", id = "1", count = 2), +// Pokit(title = "IOS", id = "2", count = 2), +// Pokit(title = "디자인", id = "3", count = 2), +// Pokit(title = "PM", id = "4", count = 1), +// Pokit(title = "서버", id = "5", count = 2) +// ) diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt index 89f19005..37662455 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt @@ -26,7 +26,8 @@ class LinkPaging( private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) override val pagingState: StateFlow = _pagingState.asStateFlow() - private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + override val pagingData: StateFlow> = _pagingData.asStateFlow() private var currentPageIndex = initPage private var requestJob: Job? = null diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt index ca171162..ab2229ce 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt @@ -23,7 +23,8 @@ class PokitPaging( private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) override val pagingState: StateFlow = _pagingState.asStateFlow() - private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + override val pagingData: StateFlow> = _pagingData.asStateFlow() private var currentPageIndex = initPage private var requestJob: Job? = null diff --git a/settings.gradle.kts b/settings.gradle.kts index 4c53243d..a6f9d425 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,3 +31,4 @@ include(":feature:login") include(":feature:pokitdetail") include(":feature:search") include(":feature:settings") +include(":feature:home")