diff --git a/.gitignore b/.gitignore index ac754dc..a8de673 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,4 @@ .externalNativeBuild .cxx local.properties - +release \ No newline at end of file diff --git a/README.md b/README.md index 85fc5d9..05197da 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,10 @@ https://play.google.com/store/apps/details?id=dev.yjyoon.kwnotice # Screenshots

- - - - + + + +


diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4e26235..a11457f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,14 +8,14 @@ plugins { android { namespace = "dev.yjyoon.kwnotice" - compileSdk = 32 + compileSdk = 33 defaultConfig { applicationId = "dev.yjyoon.kwnotice" minSdk = 24 - targetSdk = 32 - versionCode = 7 - versionName = "2.1.0" + targetSdk = 33 + versionCode = 10 + versionName = "2.2.1" } buildTypes { @@ -46,9 +46,9 @@ dependencies { implementation(project(":data")) implementation(project(":presentation")) - implementation(platform(libs.google.firebase.bom)) - implementation(libs.google.firebase.messaging) - implementation(libs.google.firebase.analytics) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.messaging) + implementation(libs.firebase.analytics) implementation(libs.hilt.android) kapt(libs.hilt.compiler) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dcbfdea..52fb5f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,9 @@ - + + + + + android:theme="@style/Theme.KWNotice"> + = runCatching { - suspendCoroutine { continuation -> + suspendCoroutine { continuation -> FirebaseMessaging.getInstance().subscribeToTopic(topic.value) .addOnCompleteListener { Log.d("fcm", "Subscribed to ${topic.value} successfully") @@ -26,7 +26,7 @@ internal class FcmSubscriptionImpl @Inject constructor() : FcmSubscription { } override suspend fun unsubscribeFrom(topic: FcmTopic): Result = runCatching { - suspendCoroutine { continuation -> + suspendCoroutine { continuation -> FirebaseMessaging.getInstance().unsubscribeFromTopic(topic.value) .addOnCompleteListener { Log.d("fcm", "Unsubscribed from ${topic.value} successfully") diff --git a/build.gradle.kts b/build.gradle.kts index 041af05..107cabb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ buildscript { dependencies { classpath(libs.android.gradle) - classpath(libs.kotlin.plugin) + classpath(libs.kotlin.gradle) classpath(libs.google.services) classpath(libs.hilt.gradle) } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index de2bc94..f041bc0 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -12,13 +12,13 @@ properties.load(project.rootProject.file("local.properties").inputStream()) android { namespace = "dev.yjyoon.kwnotice.data" - compileSdk = 32 + compileSdk = 33 defaultConfig { minSdk = 24 - targetSdk = 32 buildConfigField("String", "BASE_URL", properties["base_url"] as String) + buildConfigField("String", "KW_DORM_NOTICE_URL", properties["kw_dorm_notice_url"] as String) } compileOptions { isCoreLibraryDesugaringEnabled = true @@ -34,12 +34,12 @@ android { dependencies { implementation(project(":domain")) - implementation(libs.androidx.room.runtime) - implementation(libs.androidx.room.ktx) - annotationProcessor(libs.androidx.room.compiler) - kapt(libs.androidx.room.compiler) + implementation(libs.room.runtime) + implementation(libs.room.ktx) + annotationProcessor(libs.room.compiler) + kapt(libs.room.compiler) - implementation(libs.androidx.datastore.preferences) + implementation(libs.datastore.preferences) implementation(libs.retrofit.core) implementation(libs.retrofit.converter.gson) diff --git a/data/src/main/java/dev/yjyoon/kwnotice/data/di/DataModule.kt b/data/src/main/java/dev/yjyoon/kwnotice/data/di/DataModule.kt index e105e9e..a79be10 100644 --- a/data/src/main/java/dev/yjyoon/kwnotice/data/di/DataModule.kt +++ b/data/src/main/java/dev/yjyoon/kwnotice/data/di/DataModule.kt @@ -29,6 +29,11 @@ internal object DataModule { @Named("BaseUrl") fun provideBaseUrl(): String = BuildConfig.BASE_URL + @Provides + @Singleton + @Named("KwDormNoticeUrl") + fun provideKwDormNoticeUrl(): String = BuildConfig.KW_DORM_NOTICE_URL + @Provides @Singleton @Named("Preferences") diff --git a/data/src/main/java/dev/yjyoon/kwnotice/data/local/converter/FavoriteConverter.kt b/data/src/main/java/dev/yjyoon/kwnotice/data/local/converter/FavoriteConverter.kt index cdee6f2..52dee9f 100644 --- a/data/src/main/java/dev/yjyoon/kwnotice/data/local/converter/FavoriteConverter.kt +++ b/data/src/main/java/dev/yjyoon/kwnotice/data/local/converter/FavoriteConverter.kt @@ -15,5 +15,5 @@ object FavoriteConverter { fun dateToString(date: LocalDate): String = date.toString() @TypeConverter - fun stringToDate(string: String) = LocalDate.parse(string) + fun stringToDate(string: String): LocalDate = LocalDate.parse(string) } diff --git a/data/src/main/java/dev/yjyoon/kwnotice/data/local/dao/FavoriteDao.kt b/data/src/main/java/dev/yjyoon/kwnotice/data/local/dao/FavoriteDao.kt index db624e2..86d7989 100644 --- a/data/src/main/java/dev/yjyoon/kwnotice/data/local/dao/FavoriteDao.kt +++ b/data/src/main/java/dev/yjyoon/kwnotice/data/local/dao/FavoriteDao.kt @@ -21,6 +21,9 @@ interface FavoriteDao { @Query("SELECT id FROM favorite WHERE type = 'SwCentral'") suspend fun getSwCentralIds(): List + @Query("SELECT id FROM favorite WHERE type = 'KwDorm'") + suspend fun getKwDormIds(): List + @Query("SELECT * FROM favorite") fun getAll(): Flow> } diff --git a/data/src/main/java/dev/yjyoon/kwnotice/data/local/entity/FavoriteEntity.kt b/data/src/main/java/dev/yjyoon/kwnotice/data/local/entity/FavoriteEntity.kt index 67e7e87..88a3c13 100644 --- a/data/src/main/java/dev/yjyoon/kwnotice/data/local/entity/FavoriteEntity.kt +++ b/data/src/main/java/dev/yjyoon/kwnotice/data/local/entity/FavoriteEntity.kt @@ -15,7 +15,7 @@ data class FavoriteEntity( val date: LocalDate, val url: String ) { - enum class Type { KwHome, SwCentral } + enum class Type { KwHome, SwCentral, KwDorm, Unknown } } fun Favorite.toData() = FavoriteEntity( @@ -24,6 +24,8 @@ fun Favorite.toData() = FavoriteEntity( type = when (type) { Favorite.Type.KwHome -> FavoriteEntity.Type.KwHome Favorite.Type.SwCentral -> FavoriteEntity.Type.SwCentral + Favorite.Type.KwDorm -> FavoriteEntity.Type.KwDorm + else -> FavoriteEntity.Type.Unknown }, date = date, url = url @@ -35,6 +37,8 @@ fun FavoriteEntity.toDomain() = Favorite( type = when (type) { FavoriteEntity.Type.KwHome -> Favorite.Type.KwHome FavoriteEntity.Type.SwCentral -> Favorite.Type.SwCentral + FavoriteEntity.Type.KwDorm -> Favorite.Type.KwDorm + else -> Favorite.Type.Unknown }, date = date, url = url diff --git a/data/src/main/java/dev/yjyoon/kwnotice/data/remote/api/NoticeService.kt b/data/src/main/java/dev/yjyoon/kwnotice/data/remote/api/NoticeService.kt index b22899f..29d2024 100644 --- a/data/src/main/java/dev/yjyoon/kwnotice/data/remote/api/NoticeService.kt +++ b/data/src/main/java/dev/yjyoon/kwnotice/data/remote/api/NoticeService.kt @@ -1,8 +1,10 @@ package dev.yjyoon.kwnotice.data.remote.api +import dev.yjyoon.kwnotice.data.remote.model.KwDormNoticeResponse import dev.yjyoon.kwnotice.data.remote.model.KwHomeNoticeResponse import dev.yjyoon.kwnotice.data.remote.model.SwCentralNoticeResponse import retrofit2.http.GET +import retrofit2.http.Url internal interface NoticeService { @@ -11,4 +13,7 @@ internal interface NoticeService { @GET("sw-central") suspend fun getSwCentralNotices(): List + + @GET + suspend fun getKwDormNotices(@Url url: String): List } diff --git a/data/src/main/java/dev/yjyoon/kwnotice/data/remote/model/KwDormNoticeResponse.kt b/data/src/main/java/dev/yjyoon/kwnotice/data/remote/model/KwDormNoticeResponse.kt new file mode 100644 index 0000000..6407c35 --- /dev/null +++ b/data/src/main/java/dev/yjyoon/kwnotice/data/remote/model/KwDormNoticeResponse.kt @@ -0,0 +1,23 @@ +package dev.yjyoon.kwnotice.data.remote.model + +import com.google.gson.annotations.SerializedName +import dev.yjyoon.kwnotice.domain.model.Notice +import java.time.LocalDate + +data class KwDormNoticeResponse( + @SerializedName("id") + val id: Long, + @SerializedName("title") + val title: String, + @SerializedName("url") + val url: String, + @SerializedName("createdAt") + val postedDate: String, +) + +fun KwDormNoticeResponse.toDomain() = Notice.KwDorm( + id = id, + title = title, + url = url, + postedDate = LocalDate.parse(postedDate) +) diff --git a/data/src/main/java/dev/yjyoon/kwnotice/data/repository/FavoriteRepositoryImpl.kt b/data/src/main/java/dev/yjyoon/kwnotice/data/repository/FavoriteRepositoryImpl.kt index f178b2c..2bb7107 100644 --- a/data/src/main/java/dev/yjyoon/kwnotice/data/repository/FavoriteRepositoryImpl.kt +++ b/data/src/main/java/dev/yjyoon/kwnotice/data/repository/FavoriteRepositoryImpl.kt @@ -29,6 +29,10 @@ internal class FavoriteRepositoryImpl @Inject constructor( favoriteDao.getSwCentralIds() } + override suspend fun getFavoriteKwDormIds(): Result> = runCatching { + favoriteDao.getKwDormIds() + } + override fun getAllFavoritesStream(): Flow> = favoriteDao.getAll().map { favorites -> favorites.map { it.toDomain() } } } diff --git a/data/src/main/java/dev/yjyoon/kwnotice/data/repository/NoticeRepositoryImpl.kt b/data/src/main/java/dev/yjyoon/kwnotice/data/repository/NoticeRepositoryImpl.kt index 2d98175..79b11d2 100644 --- a/data/src/main/java/dev/yjyoon/kwnotice/data/repository/NoticeRepositoryImpl.kt +++ b/data/src/main/java/dev/yjyoon/kwnotice/data/repository/NoticeRepositoryImpl.kt @@ -4,10 +4,13 @@ import dev.yjyoon.kwnotice.data.remote.api.NoticeService import dev.yjyoon.kwnotice.data.remote.model.toDomain import dev.yjyoon.kwnotice.domain.model.Notice import dev.yjyoon.kwnotice.domain.repository.NoticeRepository +import java.time.LocalDate import javax.inject.Inject +import javax.inject.Named internal class NoticeRepositoryImpl @Inject constructor( - private val noticeService: NoticeService + private val noticeService: NoticeService, + @Named("KwDormNoticeUrl") private val kwDormNoticeUrl: String ) : NoticeRepository { override suspend fun getKwHomeNotices(): Result> = runCatching { @@ -17,4 +20,15 @@ internal class NoticeRepositoryImpl @Inject constructor( override suspend fun getSwCentralNotices(): Result> = runCatching { noticeService.getSwCentralNotices().map { it.toDomain() } } + + override suspend fun getKwDormNotices(): Result> = runCatching { + noticeService.getKwDormNotices(kwDormNoticeUrl) + .filter { + LocalDate.parse(it.postedDate) + .plusMonths(4) + .isAfter(LocalDate.now()) + } + .reversed() + .map { it.toDomain() } + } } diff --git a/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/Favorite.kt b/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/Favorite.kt index 8a08a97..67d96dd 100644 --- a/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/Favorite.kt +++ b/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/Favorite.kt @@ -13,17 +13,20 @@ data class Favorite( val text: String ) { KwHome(text = KW_HOME), - SwCentral(text = SW_CENTRAL) + SwCentral(text = SW_CENTRAL), + KwDorm(text = KW_DORM), + Unknown(text = UNKNOWN) } +} - companion object { - - fun stringToType(string: String) = when (string) { - KW_HOME -> Type.KwHome - else -> Type.SwCentral - } - - const val KW_HOME = "광운대학교" - const val SW_CENTRAL = "SW중심대학사업단" - } +fun String.toFavoriteType(): Favorite.Type = when (this) { + KW_HOME -> Favorite.Type.KwHome + SW_CENTRAL -> Favorite.Type.SwCentral + KW_DORM -> Favorite.Type.KwDorm + else -> Favorite.Type.Unknown } + +const val KW_HOME = "광운대학교" +const val SW_CENTRAL = "SW중심대학사업단" +const val KW_DORM = "빛솔재(기숙사)" +const val UNKNOWN = "UNKNOWN" diff --git a/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/FcmTopic.kt b/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/FcmTopic.kt index 9d64402..0fe689b 100644 --- a/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/FcmTopic.kt +++ b/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/FcmTopic.kt @@ -3,9 +3,13 @@ package dev.yjyoon.kwnotice.domain.model enum class FcmTopic(val value: String) { KwHomeNew(value = TOPIC_KW_HOME_NEW), KwHomeEdit(value = TOPIC_KW_HOME_EDIT), - SwCentralNew(value = TOPIC_SW_CENTRAL_NEW) + SwCentralNew(value = TOPIC_SW_CENTRAL_NEW), + KwDormCommon(value = TOPIC_KW_DORM_COMMON), + KwDormRecruitment(value = TOPIC_KW_DORM_RECRUITMENT) } private const val TOPIC_KW_HOME_NEW = "kw-home-new" private const val TOPIC_KW_HOME_EDIT = "kw-home-edit" private const val TOPIC_SW_CENTRAL_NEW = "sw-central-new" +private const val TOPIC_KW_DORM_COMMON = "kw-dorm-common" +private const val TOPIC_KW_DORM_RECRUITMENT = "kw-dorm-recruitment" diff --git a/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/Notice.kt b/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/Notice.kt index e12bb3f..e70cfb5 100644 --- a/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/Notice.kt +++ b/domain/src/main/java/dev/yjyoon/kwnotice/domain/model/Notice.kt @@ -25,6 +25,13 @@ sealed class Notice { override val url: String, override val postedDate: LocalDate, ) : Notice() + + data class KwDorm( + override val id: Long, + override val title: String, + override val url: String, + override val postedDate: LocalDate, + ) : Notice() } fun Notice.toFavorite() = when (this) { @@ -42,4 +49,11 @@ fun Notice.toFavorite() = when (this) { date = postedDate, type = Favorite.Type.SwCentral ) + is Notice.KwDorm -> Favorite( + id = id, + title = title, + url = url, + date = postedDate, + type = Favorite.Type.KwDorm + ) } diff --git a/domain/src/main/java/dev/yjyoon/kwnotice/domain/repository/FavoriteRepository.kt b/domain/src/main/java/dev/yjyoon/kwnotice/domain/repository/FavoriteRepository.kt index f410fd4..f46c6e8 100644 --- a/domain/src/main/java/dev/yjyoon/kwnotice/domain/repository/FavoriteRepository.kt +++ b/domain/src/main/java/dev/yjyoon/kwnotice/domain/repository/FavoriteRepository.kt @@ -13,5 +13,7 @@ interface FavoriteRepository { suspend fun getFavoriteSwCentralIds(): Result> + suspend fun getFavoriteKwDormIds(): Result> + fun getAllFavoritesStream(): Flow> } diff --git a/domain/src/main/java/dev/yjyoon/kwnotice/domain/repository/NoticeRepository.kt b/domain/src/main/java/dev/yjyoon/kwnotice/domain/repository/NoticeRepository.kt index 1db0081..dc52a51 100644 --- a/domain/src/main/java/dev/yjyoon/kwnotice/domain/repository/NoticeRepository.kt +++ b/domain/src/main/java/dev/yjyoon/kwnotice/domain/repository/NoticeRepository.kt @@ -7,4 +7,5 @@ interface NoticeRepository { suspend fun getKwHomeNotices(): Result> suspend fun getSwCentralNotices(): Result> + suspend fun getKwDormNotices(): Result> } diff --git a/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/favorite/GetFavoriteKwIdListUseCase.kt b/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/favorite/GetFavoriteKwIdListUseCase.kt deleted file mode 100644 index f90ea66..0000000 --- a/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/favorite/GetFavoriteKwIdListUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.yjyoon.kwnotice.domain.usecase.favorite - -import dev.yjyoon.kwnotice.domain.repository.FavoriteRepository -import javax.inject.Inject - -class GetFavoriteKwIdListUseCase @Inject constructor( - private val favoriteRepository: FavoriteRepository -) { - - suspend operator fun invoke(): Result> = - favoriteRepository.getFavoriteKwHomeIds() -} diff --git a/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/favorite/GetFavoriteSwIdListUseCase.kt b/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/favorite/GetFavoriteSwIdListUseCase.kt deleted file mode 100644 index 0285202..0000000 --- a/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/favorite/GetFavoriteSwIdListUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.yjyoon.kwnotice.domain.usecase.favorite - -import dev.yjyoon.kwnotice.domain.repository.FavoriteRepository -import javax.inject.Inject - -class GetFavoriteSwIdListUseCase @Inject constructor( - private val favoriteRepository: FavoriteRepository -) { - - suspend operator fun invoke(): Result> = - favoriteRepository.getFavoriteSwCentralIds() -} diff --git a/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/notice/GetKwDormNoticeListUseCase.kt b/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/notice/GetKwDormNoticeListUseCase.kt new file mode 100644 index 0000000..2cbf108 --- /dev/null +++ b/domain/src/main/java/dev/yjyoon/kwnotice/domain/usecase/notice/GetKwDormNoticeListUseCase.kt @@ -0,0 +1,13 @@ +package dev.yjyoon.kwnotice.domain.usecase.notice + +import dev.yjyoon.kwnotice.domain.model.Notice +import dev.yjyoon.kwnotice.domain.repository.NoticeRepository +import javax.inject.Inject + +class GetKwDormNoticeListUseCase @Inject constructor( + private val noticeRepository: NoticeRepository +) { + + suspend operator fun invoke(): Result> = + noticeRepository.getKwDormNotices() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4f781c..8ae717a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,69 +1,76 @@ [versions] -kotlin = "1.6.21" -coroutines = "1.6.3" - -android-gradle = "7.2.1" -androidx-core = "1.8.0" -androidx-lifecycle = "2.5.0" -androidx-activity = "1.5.0" -androidx-navigation = "2.4.2" - -compose = "1.2.0-beta03" -material3 = "1.0.0-alpha14" -dagger = "2.42" - -room = "2.4.2" +kotlin = "1.7.20" +coroutines = "1.6.4" + +android-gradle = "7.4.0" +androidx-core = "1.9.0" +androidx-lifecycle = "2.5.1" +androidx-activity = "1.6.1" +androidx-navigation = "2.5.3" + +compose = "1.3.3" +compose-compiler = "1.3.2" +material = "1.3.1" +material3 = "1.0.1" +dagger = "2.44" + +room = "2.5.0" datastore = "1.0.0" -google-services = "4.3.13" -firebase-bom = "30.2.0" -accompanist = "0.24.9-beta" +gms = "4.3.15" +firebase = "31.2.0" +accompanist = "0.28.0" okhttp = "4.9.3" retrofit = "2.9.0" gson = "2.9.0" -desugar = "1.1.5" +desugar = "1.2.2" junit = "4.13.2" [libraries] -kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +# Android +kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } -android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android.gradle" } -androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" } android-desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } -androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } -androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } -androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } # Compose -androidx-compose-ui-core = { module = "androidx.compose.ui:ui", version.ref = "compose" } -androidx-compose-ui-tooling-core = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } -androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } -androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } -androidx-compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } -androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0" } - -google-services = { module = "com.google.gms:google-services", version.ref = "google-services" } -google-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } -google-firebase-messaging = { module = "com.google.firebase:firebase-messaging" } -google-firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } - -google-accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } -google-accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" } -google-accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" } -google-accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } +compose-ui-core = { module = "androidx.compose.ui:ui", version.ref = "compose" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +compose-ui-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } +compose-material = { module = "androidx.compose.material:material", version.ref = "material" } +compose-material-icon = { module = "androidx.compose.material:material-icon-core", version.ref = "material" } +compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } + +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } +lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0" } + +# Firebase +google-services = { module = "com.google.gms:google-services", version.ref = "gms" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } + +accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } +accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" } +accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" } +accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } # Room -androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } -androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } -androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } # Datastore -androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } +datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } # Network retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } @@ -79,15 +86,16 @@ hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "dagger" } hilt-gradle = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "dagger" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "dagger" } - - javax-inject = "javax.inject:javax.inject:1" [bundles] -compose = ["androidx-compose-ui-core", - "androidx-compose-ui-tooling-core", - "androidx-compose-ui-tooling-preview", - "androidx-compose-material3", - "androidx-activity-compose", - "androidx-compose-navigation", - "androidx-hilt-navigation-compose"] \ No newline at end of file +compose = [ + "compose-ui-core", + "compose-ui-preview", + "compose-material", + "compose-material3", + "activity-compose", + "navigation-compose", + "lifecycle-viewmodel-compose", + "hilt-navigation-compose" +] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8f6c285..51a71c2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Jul 01 20:05:26 KST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 3880917..112344b 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -7,11 +7,10 @@ plugins { android { namespace = "dev.yjyoon.kwnotice.presentation" - compileSdk = 32 + compileSdk = 33 defaultConfig { minSdk = 24 - targetSdk = 32 vectorDrawables { useSupportLibrary = true @@ -21,7 +20,7 @@ android { compose = true } composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.get() + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } compileOptions { isCoreLibraryDesugaringEnabled = true @@ -31,7 +30,8 @@ android { } kotlinOptions { freeCompilerArgs += listOf( - "-Xopt-in=kotlin.RequiresOptIn", + "-X opt-in=kotlin.RequiresOptIn", + "-opt-in=androidx.compose.material.ExperimentalMaterialApi", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", ) @@ -42,18 +42,20 @@ android { dependencies { implementation(project(":domain")) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime) + implementation(libs.androidx.core) + implementation(libs.lifecycle.runtime) implementation(libs.bundles.compose) - implementation(libs.google.accompanist.webview) - implementation(libs.google.accompanist.systemuicontroller) - implementation(libs.google.accompanist.navigation.animation) - implementation(libs.google.accompanist.pager) + implementation(libs.accompanist.webview) + implementation(libs.accompanist.systemuicontroller) + implementation(libs.accompanist.navigation.animation) + implementation(libs.accompanist.pager) implementation(libs.hilt.android) kapt(libs.hilt.compiler) coreLibraryDesugaring(libs.android.desugar) + + debugImplementation(libs.compose.ui.tooling) } \ No newline at end of file diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeDropdownMenu.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeDropdownMenu.kt index 3bf7602..3c7d0b6 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeDropdownMenu.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeDropdownMenu.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import dev.yjyoon.kwnotice.presentation.R @@ -24,6 +25,7 @@ import dev.yjyoon.kwnotice.presentation.ui.theme.KwNoticeTheme @Composable fun KwNoticeDropdownMenu( + modifier: Modifier = Modifier, @DrawableRes leadingIconRes: Int, initialItem: String, items: List, @@ -33,7 +35,7 @@ fun KwNoticeDropdownMenu( var selectedItem by remember { mutableStateOf(initialItem) } Box( - Modifier.wrapContentSize() + modifier.wrapContentSize() ) { FilterChip( selected = expanded, @@ -41,7 +43,9 @@ fun KwNoticeDropdownMenu( label = { Text( text = selectedItem, - style = MaterialTheme.typography.bodySmall + style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis, + maxLines = 1 ) }, leadingIcon = { @@ -50,40 +54,39 @@ fun KwNoticeDropdownMenu( contentDescription = null, modifier = Modifier.size(12.dp) ) - }, - selectedIcon = { - Icon( - painter = painterResource(id = leadingIconRes), - contentDescription = null, - modifier = Modifier.size(12.dp) - ) } ) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) { - DropdownMenuItem(text = { - Text( - text = initialItem, - style = MaterialTheme.typography.bodySmall - ) - }, onClick = { - selectedItem = initialItem - expanded = false - onSelectItem(null) - }) - items.forEach { - DropdownMenuItem(text = { + DropdownMenuItem( + text = { Text( - text = it, + text = initialItem, style = MaterialTheme.typography.bodySmall ) - }, onClick = { - selectedItem = it + }, + onClick = { + selectedItem = initialItem expanded = false - onSelectItem(it) - }) + onSelectItem(null) + } + ) + items.forEach { + DropdownMenuItem( + text = { + Text( + text = it, + style = MaterialTheme.typography.bodySmall + ) + }, + onClick = { + selectedItem = it + expanded = false + onSelectItem(it) + } + ) } } } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeSearchBar.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeSearchBar.kt index 6ee59aa..a8d6444 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeSearchBar.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeSearchBar.kt @@ -2,30 +2,33 @@ package dev.yjyoon.kwnotice.presentation.ui.component import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -33,6 +36,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import dev.yjyoon.kwnotice.presentation.R import dev.yjyoon.kwnotice.presentation.ui.theme.KwNoticeTheme +import dev.yjyoon.kwnotice.presentation.ui.theme.KwNoticeTypography @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -47,51 +51,46 @@ fun KwNoticeSearchBar( val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } - Box { - TextField( - value = text, - onValueChange = { text = it }, - modifier = modifier - .padding(horizontal = 18.dp, vertical = 6.dp) - .focusRequester(focusRequester), - textStyle = MaterialTheme.typography.bodyMedium, - placeholder = { - Text( - stringResource(id = R.string.searchbar_placeholder), - color = color.copy(alpha = 0.15f), - style = MaterialTheme.typography.bodyMedium, - maxLines = 1 - ) - }, - leadingIcon = { Icon(imageVector = Icons.Default.Search, contentDescription = null) }, - trailingIcon = { - IconButton(onClick = { - keyboardController?.hide() - onClose() - }) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = null - ) + BasicTextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier.focusRequester(focusRequester), + textStyle = KwNoticeTypography.bodyMedium, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions( + onSearch = { + onSearch(text) + keyboardController?.hide() + } + ), + singleLine = true, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + decorationBox = { innerTextField -> + Surface( + shape = RoundedCornerShape(12.dp), + color = color.copy(alpha = 0.05f) + ) { + Row( + modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(imageVector = Icons.Default.Search, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Box(modifier = Modifier.weight(1f)) { + if(text.isEmpty()) { + Text( + stringResource(id = R.string.searchbar_placeholder), + color = color.copy(alpha = 0.15f), + style = KwNoticeTypography.bodyMedium, + maxLines = 1 + ) + } + innerTextField() + } } - }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { - onSearch(text) - keyboardController?.hide() - } - ), - singleLine = true, - shape = RoundedCornerShape(12.dp), - colors = TextFieldDefaults.textFieldColors( - containerColor = color.copy(alpha = 0.05f), - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ) - ) - } + } + } + ) LaunchedEffect(Unit) { focusRequester.requestFocus() diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeSwitchBar.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeSwitchBar.kt index 1a5adfc..39502db 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeSwitchBar.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeSwitchBar.kt @@ -27,19 +27,6 @@ fun KwNoticeSwitchBar( onTap: (Boolean) -> Unit, modifier: Modifier = Modifier ) { - val icon: (@Composable () -> Unit)? = if (checked) { - { - Icon( - imageVector = Icons.Rounded.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(SwitchDefaults.IconSize), - ) - } - } else { - null - } - Row( modifier.then(Modifier .height(56.dp) @@ -51,8 +38,7 @@ fun KwNoticeSwitchBar( Text(title) Switch( checked = checked, - onCheckedChange = { onTap(it) }, - thumbContent = icon + onCheckedChange = { onTap(it) } ) } } @@ -67,4 +53,4 @@ private fun KwNoticeSwitchBarPreview() { onTap = { } ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeTopAppBar.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeTopAppBar.kt index 302d1c1..1c53520 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeTopAppBar.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/component/KwNoticeTopAppBar.kt @@ -7,35 +7,71 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.with +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import dev.yjyoon.kwnotice.presentation.R -import dev.yjyoon.kwnotice.presentation.ui.theme.KwNoticeTheme +import androidx.compose.ui.unit.dp import dev.yjyoon.kwnotice.presentation.ui.theme.KwNoticeTypography + @Composable fun KwNoticeTopAppBar( + title: @Composable() (RowScope.() -> Unit), + actions: @Composable() (RowScope.() -> Unit) +) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 0.dp, start = 4.dp, end = 4.dp), + horizontalArrangement = Arrangement.End, + content = actions + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 0.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + content = title + ) + } +} + +@Composable +fun KwNoticeSimpleTopAppBar( titleText: String, actionIcon: ImageVector, onActionClick: () -> Unit ) { - SmallTopAppBar( - title = { Text(text = titleText, style = KwNoticeTypography.titleLarge) }, + KwNoticeTopAppBar( + title = { + Text( + modifier = Modifier.padding(vertical = 8.dp), + text = titleText, + style = KwNoticeTypography.titleLarge, + maxLines = 1 + ) + }, actions = { IconButton(onClick = onActionClick) { Icon(imageVector = actionIcon, contentDescription = null) @@ -48,19 +84,19 @@ fun KwNoticeTopAppBar( fun KwNoticeSearchTopAppBar( titleText: String, onSearch: (String) -> Unit, - onCloseSearh: () -> Unit + onCloseSearch: () -> Unit ) { var showSearchBar by remember { mutableStateOf(false) } - SmallTopAppBar( + KwNoticeTopAppBar( title = { Text( + modifier = Modifier.padding(vertical = 8.dp), text = titleText, style = KwNoticeTypography.titleLarge, maxLines = 1 ) - }, - actions = { + Spacer(modifier = Modifier.width(16.dp)) AnimatedContent( targetState = showSearchBar, transitionSpec = { @@ -78,30 +114,22 @@ fun KwNoticeSearchTopAppBar( Modifier.fillMaxWidth(), onSearch = onSearch, onClose = { - onCloseSearh() + onCloseSearch() showSearchBar = false } ) - } else { - IconButton(onClick = { - showSearchBar = true - }) { - Icon(imageVector = Icons.Default.Search, contentDescription = null) - } } } + }, + actions = { + IconButton( + onClick = { showSearchBar = !showSearchBar } + ) { + Icon( + imageVector = if (showSearchBar) Icons.Default.Close else Icons.Default.Search, + contentDescription = null + ) + } } ) } - -@Preview -@Composable -private fun KwNoticeTopAppBarPreview() { - KwNoticeTheme { - KwNoticeTopAppBar( - titleText = stringResource(id = R.string.navigation_notice), - actionIcon = Icons.Outlined.Search, - onActionClick = {} - ) - } -} \ No newline at end of file diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteCard.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteCard.kt index 5bcb1a1..0d69d16 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteCard.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteCard.kt @@ -26,6 +26,7 @@ import dev.yjyoon.kwnotice.domain.model.Favorite import dev.yjyoon.kwnotice.presentation.R import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeBadge import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeRoundRect +import dev.yjyoon.kwnotice.presentation.ui.util.DateDisplayUtil.toRelativeDateString import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -95,7 +96,7 @@ fun NoticeTitle(title: String) { fun NoticeDateBadge(date: LocalDate) { KwNoticeBadge( leadingIconRes = R.drawable.ic_calendar, - label = date.format(DateTimeFormatter.ofPattern(stringResource(id = R.string.notice_date_format))) + label = date.toRelativeDateString() ) } @@ -104,6 +105,8 @@ fun NoticeTypeBadge(type: Favorite.Type) { @StringRes val typeStringRes = when (type) { Favorite.Type.KwHome -> R.string.kw_home Favorite.Type.SwCentral -> R.string.sw_central + Favorite.Type.KwDorm -> R.string.kw_dorm + else -> R.string.unknown } KwNoticeBadge(leadingIconRes = R.drawable.ic_tag, label = stringResource(id = typeStringRes)) diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteScreen.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteScreen.kt index b2db156..623a8e9 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteScreen.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -76,6 +77,7 @@ fun FavoriteScreen( onInitFilter: () -> Unit, addToFavorite: (Favorite) -> Unit ) { + val context = LocalContext.current val scope = rememberCoroutineScope() var job: Job? by remember { mutableStateOf(null) } val snackbarHostState = remember { SnackbarHostState() } @@ -84,8 +86,8 @@ fun FavoriteScreen( job?.cancel() job = scope.launch { val snackbarResult = snackbarHostState.showSnackbar( - message = "즐겨찾기에서 삭제했습니다", - actionLabel = "되돌리기" + message = context.getString(R.string.unbookmarked), + actionLabel = context.getString(R.string.undo) ) when (snackbarResult) { SnackbarResult.ActionPerformed -> { @@ -111,7 +113,7 @@ fun FavoriteScreen( KwNoticeSearchTopAppBar( titleText = stringResource(id = R.string.navigation_favorite), onSearch = onSearch, - onCloseSearh = onInitFilter + onCloseSearch = onInitFilter ) Box( Modifier.weight(1f) diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteViewModel.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteViewModel.kt index ffec77e..5b060a6 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteViewModel.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/favorite/FavoriteViewModel.kt @@ -3,6 +3,7 @@ package dev.yjyoon.kwnotice.presentation.ui.favorite import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.yjyoon.kwnotice.domain.model.Favorite +import dev.yjyoon.kwnotice.domain.model.toFavoriteType import dev.yjyoon.kwnotice.domain.usecase.favorite.AddFavoriteUseCase import dev.yjyoon.kwnotice.domain.usecase.favorite.DeleteFavoriteUseCase import dev.yjyoon.kwnotice.domain.usecase.favorite.GetAllFavoriteListUseCase @@ -63,7 +64,7 @@ class FavoriteViewModel @Inject constructor( fun setTypeFilter(type: String?) { _filterState.update { - it.copy(type = type?.let { type -> Favorite.Companion.stringToType(type) }) + it.copy(type = type?.toFavoriteType()) } } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/model/FcmTopicModel.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/model/FcmTopicModel.kt index 6a6969a..d63713b 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/model/FcmTopicModel.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/model/FcmTopicModel.kt @@ -10,5 +10,7 @@ data class FcmTopicModel(val topic: FcmTopic) { FcmTopic.KwHomeNew -> R.string.settings_notification_new FcmTopic.KwHomeEdit -> R.string.settings_notification_edit FcmTopic.SwCentralNew -> R.string.settings_notification_new + FcmTopic.KwDormCommon -> R.string.settings_notification_common + FcmTopic.KwDormRecruitment -> R.string.settings_notification_recruit } } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/FailureScreen.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/FailureScreen.kt index d4bc633..e9dc6f0 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/FailureScreen.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/FailureScreen.kt @@ -1,8 +1,15 @@ package dev.yjyoon.kwnotice.presentation.ui.notice +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -18,21 +25,35 @@ import dev.yjyoon.kwnotice.presentation.R import dev.yjyoon.kwnotice.presentation.ui.theme.KwNoticeTheme @Composable -fun FailureScreen() { - Column( - horizontalAlignment = Alignment.CenterHorizontally +fun FailureScreen( + onRefresh: () -> Unit +) { + Box( + Modifier.fillMaxSize().padding(24.dp) ) { - Icon( - painter = painterResource(id = R.drawable.ic_wifi_off), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(Modifier.height(12.dp)) - Text( - text = stringResource(id = R.string.notice_network_fail), - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + Column( + Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.ic_wifi_off), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(32.dp) + ) + Spacer(Modifier.height(12.dp)) + Text( + text = stringResource(id = R.string.notice_network_fail), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + FloatingActionButton( + onClick = onRefresh, + modifier = Modifier.align(Alignment.BottomEnd) + ) { + Icon(imageVector = Icons.Default.Refresh, contentDescription = null) + } } } @@ -40,6 +61,6 @@ fun FailureScreen() { @Composable private fun FailureScreenPreview() { KwNoticeTheme { - FailureScreen() + FailureScreen(onRefresh = {}) } } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/KwDormContent.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/KwDormContent.kt new file mode 100644 index 0000000..27cc0e6 --- /dev/null +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/KwDormContent.kt @@ -0,0 +1,145 @@ +package dev.yjyoon.kwnotice.presentation.ui.notice + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.yjyoon.kwnotice.domain.model.Favorite +import dev.yjyoon.kwnotice.domain.model.Notice +import dev.yjyoon.kwnotice.domain.model.toFavorite +import dev.yjyoon.kwnotice.presentation.R +import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeDropdownMenu + +@Composable +fun KwDormContent( + uiState: KwDormNoticeUiState, + filterState: NoticeFilterState, + favoriteNotices: List, + refreshing: Boolean, + onClickNotice: (String) -> Unit, + onAddToFavorite: (Notice) -> Unit, + onDeleteFromFavorite: (Notice) -> Unit, + onMonthFilterChange: (String?) -> Unit, + onRefresh: () -> Unit +) { + when (uiState) { + is KwDormNoticeUiState.Success -> { + Column(Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.End + ) { + KwNoticeDropdownMenu( + leadingIconRes = R.drawable.ic_tag, + initialItem = stringResource(id = R.string.filter_tag_all), + items = listOf(), + onSelectItem = { } + ) + Spacer(Modifier.width(8.dp)) + KwNoticeDropdownMenu( + leadingIconRes = R.drawable.ic_group, + initialItem = stringResource(id = R.string.filter_department_all), + items = listOf(), + onSelectItem = { } + ) + Spacer(Modifier.weight(1f)) + KwNoticeDropdownMenu( + leadingIconRes = R.drawable.ic_calendar, + initialItem = stringResource(id = R.string.filter_month_all), + items = uiState.months.map { "$it${stringResource(id = R.string.month)}" }, + onSelectItem = onMonthFilterChange + ) + } + KwDormNoticeColumn( + uiState = uiState, + filterState = filterState, + favoriteNotices = favoriteNotices, + refreshing = refreshing, + onClickNotice = onClickNotice, + onAddToFavorite = onAddToFavorite, + onDeleteFromFavorite = onDeleteFromFavorite, + onRefresh = onRefresh + ) + } + } + KwDormNoticeUiState.Loading -> { + CircularProgressIndicator() + } + KwDormNoticeUiState.Failure -> { + FailureScreen(onRefresh = onRefresh) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun KwDormNoticeColumn( + uiState: KwDormNoticeUiState.Success, + filterState: NoticeFilterState, + favoriteNotices: List, + refreshing: Boolean, + onClickNotice: (String) -> Unit, + onAddToFavorite: (Notice) -> Unit, + onDeleteFromFavorite: (Notice) -> Unit, + onRefresh: () -> Unit +) { + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = onRefresh + ) + + Box( + modifier = Modifier.pullRefresh(pullRefreshState) + ) { + LazyColumn( + Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 18.dp, vertical = 0.dp), + verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.Top) + ) { + items(uiState.notices.filter { filterState.filtering(it) }) { + NoticeCard( + notice = it, + onClickNotice = onClickNotice, + bookmarked = favoriteNotices.contains(it.toFavorite()), + onToggleBookmark = { notice, bookmarked -> + if (bookmarked) { + onAddToFavorite(notice) + } else { + onDeleteFromFavorite(notice) + } + } + ) + } + item { Spacer(modifier = Modifier.height(4.dp)) } + } + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + refreshing = refreshing, + state = pullRefreshState, + contentColor = MaterialTheme.colorScheme.primary, + scale = true + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/KwHomeContent.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/KwHomeContent.kt index 16f8d7b..7413140 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/KwHomeContent.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/KwHomeContent.kt @@ -1,16 +1,23 @@ package dev.yjyoon.kwnotice.presentation.ui.notice import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -27,12 +34,14 @@ fun KwHomeContent( uiState: KwHomeNoticeUiState, filterState: NoticeFilterState, favoriteNotices: List, + refreshing: Boolean, onClickNotice: (String) -> Unit, onAddToFavorite: (Notice) -> Unit, onDeleteFromFavorite: (Notice) -> Unit, onTagFilterChange: (String?) -> Unit, onDepartmentFilterChange: (String?) -> Unit, - onMonthFilterChange: (String?) -> Unit + onMonthFilterChange: (String?) -> Unit, + onRefresh: () -> Unit ) { when (uiState) { is KwHomeNoticeUiState.Success -> { @@ -48,6 +57,7 @@ fun KwHomeContent( ) Spacer(Modifier.width(8.dp)) KwNoticeDropdownMenu( + modifier = Modifier.padding(end = 8.dp), leadingIconRes = R.drawable.ic_group, initialItem = stringResource(id = R.string.filter_department_all), items = uiState.departments, @@ -64,10 +74,12 @@ fun KwHomeContent( KwHomeNoticeColumn( uiState = uiState, filterState = filterState, + favoriteNotices = favoriteNotices, + refreshing = refreshing, onClickNotice = onClickNotice, onAddToFavorite = onAddToFavorite, onDeleteFromFavorite = onDeleteFromFavorite, - favoriteNotices = favoriteNotices + onRefresh = onRefresh ) } @@ -76,36 +88,57 @@ fun KwHomeContent( CircularProgressIndicator() } KwHomeNoticeUiState.Failure -> { - FailureScreen() + FailureScreen(onRefresh = onRefresh) } } } +@OptIn(ExperimentalMaterialApi::class) @Composable fun KwHomeNoticeColumn( uiState: KwHomeNoticeUiState.Success, filterState: NoticeFilterState, favoriteNotices: List, + refreshing: Boolean, onClickNotice: (String) -> Unit, onAddToFavorite: (Notice) -> Unit, - onDeleteFromFavorite: (Notice) -> Unit + onDeleteFromFavorite: (Notice) -> Unit, + onRefresh: () -> Unit ) { - LazyColumn( - contentPadding = PaddingValues(horizontal = 18.dp, vertical = 0.dp), - verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.Top), + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = onRefresh + ) + + Box( + modifier = Modifier.pullRefresh(pullRefreshState) ) { - items(uiState.notices.filter { filterState.filtering(it) }) { - NoticeCard( - notice = it, - onClickNotice = onClickNotice, - bookmarked = favoriteNotices.contains(it.toFavorite()), - onToggleBookmark = { notice, bookmarked -> - if (bookmarked) { - onAddToFavorite(notice) - } else { - onDeleteFromFavorite(notice) + LazyColumn( + contentPadding = PaddingValues(horizontal = 18.dp, vertical = 0.dp), + verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.Top), + ) { + items(uiState.notices.filter { filterState.filtering(it) }) { + NoticeCard( + notice = it, + onClickNotice = onClickNotice, + bookmarked = favoriteNotices.contains(it.toFavorite()), + onToggleBookmark = { notice, bookmarked -> + if (bookmarked) { + onAddToFavorite(notice) + } else { + onDeleteFromFavorite(notice) + } } - }) + ) + } + item { Spacer(modifier = Modifier.height(4.dp)) } } + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + refreshing = refreshing, + state = pullRefreshState, + contentColor = MaterialTheme.colorScheme.primary, + scale = true + ) } } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeCard.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeCard.kt index 3d7fbb0..9fc8fa3 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeCard.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeCard.kt @@ -17,7 +17,6 @@ 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.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -27,8 +26,8 @@ import dev.yjyoon.kwnotice.presentation.R import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeBadge import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeRoundRect import dev.yjyoon.kwnotice.presentation.ui.theme.KwNoticeTheme +import dev.yjyoon.kwnotice.presentation.ui.util.DateDisplayUtil.toRelativeDateString import java.time.LocalDate -import java.time.format.DateTimeFormatter @Composable fun NoticeCard( @@ -89,7 +88,7 @@ fun NoticeTitle(notice: Notice) { is Notice.KwHome -> { notice.title.substring(notice.tag.length + 3) } - is Notice.SwCentral -> { + else -> { notice.title } } @@ -109,14 +108,14 @@ fun NoticeDateBadge(notice: Notice) { is Notice.KwHome -> { notice.modifiedDate } - is Notice.SwCentral -> { + else -> { notice.postedDate } } KwNoticeBadge( leadingIconRes = R.drawable.ic_calendar, - label = date.format(DateTimeFormatter.ofPattern(stringResource(id = R.string.notice_date_format))) + label = date.toRelativeDateString() ) } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeFilterState.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeFilterState.kt index 069fa65..a409114 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeFilterState.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeFilterState.kt @@ -16,7 +16,7 @@ data class NoticeFilterState( .and(department == null || department == notice.department) .and(month == null || month.dropLast(1).toInt() == notice.modifiedDate.monthValue) } - is Notice.SwCentral -> { + else -> { (title == null || title in notice.title) .and(month == null || month.dropLast(1).toInt() == notice.postedDate.monthValue) } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeScreen.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeScreen.kt index 8eb2e17..53b2323 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeScreen.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeScreen.kt @@ -40,6 +40,7 @@ fun NoticeScreen( uiState = uiState, filterState = filterState, favoriteNotices = uiState.favoriteNotices, + refreshing = viewModel.refreshing, onClickNotice = onClickNotice, onAddToFavorite = viewModel::addFavorite, onDeleteFromFavorite = viewModel::deleteFavorite, @@ -47,7 +48,8 @@ fun NoticeScreen( onTagFilterChange = viewModel::setTagFilter, onDepartmentFilterChange = viewModel::setDepartmentFilter, onMonthFilterChange = viewModel::setMonthFilter, - onInitFilter = viewModel::initFilter + onInitFilter = viewModel::initFilter, + onRefresh = viewModel::refresh ) } @@ -56,6 +58,7 @@ fun NoticeScreen( fun NoticeScreen( uiState: NoticeUiState, filterState: NoticeFilterState, + refreshing: Boolean, favoriteNotices: List, onClickNotice: (String) -> Unit, onAddToFavorite: (Notice) -> Unit, @@ -64,7 +67,8 @@ fun NoticeScreen( onTagFilterChange: (String?) -> Unit, onDepartmentFilterChange: (String?) -> Unit, onMonthFilterChange: (String?) -> Unit, - onInitFilter: () -> Unit + onInitFilter: () -> Unit, + onRefresh: (NoticeTab) -> Unit, ) { val pagerState = rememberPagerState() val coroutineScope = rememberCoroutineScope() @@ -78,7 +82,7 @@ fun NoticeScreen( KwNoticeSearchTopAppBar( titleText = stringResource(id = R.string.navigation_notice), onSearch = onSearch, - onCloseSearh = onInitFilter + onCloseSearch = onInitFilter ) TabRow( selectedTabIndex = pagerState.currentPage, @@ -111,7 +115,9 @@ fun NoticeScreen( filterState = filterState, onTagFilterChange = onTagFilterChange, onDepartmentFilterChange = onDepartmentFilterChange, - onMonthFilterChange = onMonthFilterChange + onMonthFilterChange = onMonthFilterChange, + refreshing = refreshing, + onRefresh = { onRefresh(NoticeTab.KwHome) } ) } NoticeTab.SwCentral.ordinal -> { @@ -122,7 +128,22 @@ fun NoticeScreen( onDeleteFromFavorite = onDeleteFromFavorite, favoriteNotices = favoriteNotices, filterState = filterState, - onMonthFilterChange = onMonthFilterChange + onMonthFilterChange = onMonthFilterChange, + refreshing = refreshing, + onRefresh = { onRefresh(NoticeTab.SwCentral) } + ) + } + NoticeTab.KwDorm.ordinal -> { + KwDormContent( + uiState = uiState.kwDormNoticeUiState, + onClickNotice = onClickNotice, + onAddToFavorite = onAddToFavorite, + onDeleteFromFavorite = onDeleteFromFavorite, + favoriteNotices = favoriteNotices, + filterState = filterState, + onMonthFilterChange = onMonthFilterChange, + refreshing = refreshing, + onRefresh = { onRefresh(NoticeTab.KwDorm) } ) } } @@ -154,10 +175,12 @@ private fun NoticeScreenPreview() { months = listOf(1) ), swCentralNoticeUiState = SwCentralNoticeUiState.Failure, + kwDormNoticeUiState = KwDormNoticeUiState.Failure, favoriteNotices = emptyList() ), filterState = NoticeFilterState.Unspecified, favoriteNotices = emptyList(), + refreshing = false, onClickNotice = {}, onAddToFavorite = {}, onDeleteFromFavorite = {}, @@ -165,7 +188,8 @@ private fun NoticeScreenPreview() { onTagFilterChange = {}, onSearch = {}, onMonthFilterChange = {}, - onInitFilter = {} + onInitFilter = {}, + onRefresh = {} ) } } \ No newline at end of file diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeTab.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeTab.kt index abfea17..970b5fb 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeTab.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeTab.kt @@ -7,5 +7,6 @@ enum class NoticeTab( @StringRes val textRes: Int ) { KwHome(textRes = R.string.kw_home), - SwCentral(textRes = R.string.sw_central) + SwCentral(textRes = R.string.sw_central_short), + KwDorm(textRes = R.string.kw_dorm_short) } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeUiState.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeUiState.kt index 6751eea..6915a5a 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeUiState.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeUiState.kt @@ -4,15 +4,17 @@ import dev.yjyoon.kwnotice.domain.model.Favorite import dev.yjyoon.kwnotice.domain.model.Notice data class NoticeUiState( - val kwHomeNoticeUiState: KwHomeNoticeUiState, - val swCentralNoticeUiState: SwCentralNoticeUiState, - val favoriteNotices: List + var kwHomeNoticeUiState: KwHomeNoticeUiState, + var swCentralNoticeUiState: SwCentralNoticeUiState, + var kwDormNoticeUiState: KwDormNoticeUiState, + var favoriteNotices: List ) { companion object { val Loading = NoticeUiState( kwHomeNoticeUiState = KwHomeNoticeUiState.Loading, swCentralNoticeUiState = SwCentralNoticeUiState.Loading, + kwDormNoticeUiState = KwDormNoticeUiState.Loading, favoriteNotices = emptyList() ) } @@ -39,3 +41,13 @@ sealed interface SwCentralNoticeUiState { object Loading : SwCentralNoticeUiState object Failure : SwCentralNoticeUiState } + +sealed interface KwDormNoticeUiState { + data class Success( + val notices: List, + val months: List + ) : KwDormNoticeUiState + + object Loading : KwDormNoticeUiState + object Failure : KwDormNoticeUiState +} diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeViewModel.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeViewModel.kt index 277c0e4..d7e7a3e 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeViewModel.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/NoticeViewModel.kt @@ -1,5 +1,8 @@ package dev.yjyoon.kwnotice.presentation.ui.notice +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.yjyoon.kwnotice.domain.model.Notice @@ -7,9 +10,11 @@ import dev.yjyoon.kwnotice.domain.model.toFavorite import dev.yjyoon.kwnotice.domain.usecase.favorite.AddFavoriteUseCase import dev.yjyoon.kwnotice.domain.usecase.favorite.DeleteFavoriteUseCase import dev.yjyoon.kwnotice.domain.usecase.favorite.GetAllFavoriteListUseCase +import dev.yjyoon.kwnotice.domain.usecase.notice.GetKwDormNoticeListUseCase import dev.yjyoon.kwnotice.domain.usecase.notice.GetKwHomeNoticeListUseCase import dev.yjyoon.kwnotice.domain.usecase.notice.GetSwCentralNoticeListUseCase import dev.yjyoon.kwnotice.presentation.ui.base.BaseViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -18,6 +23,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -25,11 +31,12 @@ class NoticeViewModel @Inject constructor( getAllFavoriteListUseCase: GetAllFavoriteListUseCase, private val getKwHomeNoticeListUseCase: GetKwHomeNoticeListUseCase, private val getSwCentralNoticeListUseCase: GetSwCentralNoticeListUseCase, + private val getKwDormNoticeListUseCase: GetKwDormNoticeListUseCase, private val addFavoriteUseCase: AddFavoriteUseCase, private val deleteFavoriteUseCase: DeleteFavoriteUseCase ) : BaseViewModel() { - val uiState: StateFlow = combine( + var uiState: StateFlow = combine( flow { getKwHomeNoticeListUseCase() .onSuccess { emit(it) } @@ -40,8 +47,13 @@ class NoticeViewModel @Inject constructor( .onSuccess { emit(it) } .onFailure { emit(null) } }, + flow { + getKwDormNoticeListUseCase() + .onSuccess { emit(it) } + .onFailure { emit(null) } + }, getAllFavoriteListUseCase() - ) { kwHomeNotices, swCentralNotices, favoriteNotices -> + ) { kwHomeNotices, swCentralNotices, kwDormNotices, favoriteNotices -> NoticeUiState( kwHomeNoticeUiState = if (kwHomeNotices != null) { KwHomeNoticeUiState.Success( @@ -61,6 +73,14 @@ class NoticeViewModel @Inject constructor( } else { SwCentralNoticeUiState.Failure }, + kwDormNoticeUiState = if (kwDormNotices != null) { + KwDormNoticeUiState.Success( + notices = kwDormNotices, + months = kwDormNotices.map { it.postedDate.monthValue }.distinct() + ) + } else { + KwDormNoticeUiState.Failure + }, favoriteNotices = favoriteNotices ) }.stateIn(viewModelScope, SharingStarted.Eagerly, NoticeUiState.Loading) @@ -68,6 +88,8 @@ class NoticeViewModel @Inject constructor( private val _filterState = MutableStateFlow(NoticeFilterState.Unspecified) val filterState: StateFlow = _filterState.asStateFlow() + var refreshing by mutableStateOf(false) + fun addFavorite(notice: Notice) { launch { addFavoriteUseCase(notice.toFavorite()).getOrThrow() @@ -107,4 +129,66 @@ class NoticeViewModel @Inject constructor( fun initFilter() { _filterState.value = NoticeFilterState.Unspecified } + + fun refresh(tab: NoticeTab) { + when (tab) { + NoticeTab.KwHome -> { + viewModelScope.launch { + refreshing = true + getKwHomeNoticeListUseCase() + .onSuccess { kwHomeNotices -> + uiState.value.kwHomeNoticeUiState = KwHomeNoticeUiState.Success( + notices = kwHomeNotices, + tags = kwHomeNotices.map { it.tag }.distinct(), + departments = kwHomeNotices.map { it.department }.distinct(), + months = kwHomeNotices.map { it.modifiedDate.monthValue }.distinct() + ) + } + .onFailure { + uiState.value.kwHomeNoticeUiState = KwHomeNoticeUiState.Failure + } + delay(250L) + refreshing = false + } + } + NoticeTab.SwCentral -> { + viewModelScope.launch { + refreshing = true + getSwCentralNoticeListUseCase() + .onSuccess { swCentralNotices -> + uiState.value.swCentralNoticeUiState = SwCentralNoticeUiState.Success( + notices = swCentralNotices, + months = swCentralNotices + .map { it.postedDate.monthValue } + .distinct() + ) + } + .onFailure { + uiState.value.swCentralNoticeUiState = SwCentralNoticeUiState.Failure + } + delay(250L) + refreshing = false + } + } + NoticeTab.KwDorm -> { + viewModelScope.launch { + refreshing = true + getKwDormNoticeListUseCase() + .onSuccess { kwDormNotices -> + uiState.value.kwDormNoticeUiState = KwDormNoticeUiState.Success( + notices = kwDormNotices, + months = kwDormNotices + .map { it.postedDate.monthValue } + .distinct() + ) + } + .onFailure { + uiState.value.kwDormNoticeUiState = KwDormNoticeUiState.Failure + } + delay(250L) + refreshing = false + } + } + } + } } diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/SwCentralContent.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/SwCentralContent.kt index 3281d61..d003adb 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/SwCentralContent.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/notice/SwCentralContent.kt @@ -1,15 +1,24 @@ package dev.yjyoon.kwnotice.presentation.ui.notice import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,10 +35,12 @@ fun SwCentralContent( uiState: SwCentralNoticeUiState, filterState: NoticeFilterState, favoriteNotices: List, + refreshing: Boolean, onClickNotice: (String) -> Unit, onAddToFavorite: (Notice) -> Unit, onDeleteFromFavorite: (Notice) -> Unit, - onMonthFilterChange: (String?) -> Unit + onMonthFilterChange: (String?) -> Unit, + onRefresh: () -> Unit ) { when (uiState) { is SwCentralNoticeUiState.Success -> { @@ -40,6 +51,20 @@ fun SwCentralContent( .padding(horizontal = 18.dp, vertical = 8.dp), horizontalArrangement = Arrangement.End ) { + KwNoticeDropdownMenu( + leadingIconRes = R.drawable.ic_tag, + initialItem = stringResource(id = R.string.filter_tag_all), + items = listOf(), + onSelectItem = { } + ) + Spacer(Modifier.width(8.dp)) + KwNoticeDropdownMenu( + leadingIconRes = R.drawable.ic_group, + initialItem = stringResource(id = R.string.filter_department_all), + items = listOf(), + onSelectItem = { } + ) + Spacer(Modifier.weight(1f)) KwNoticeDropdownMenu( leadingIconRes = R.drawable.ic_calendar, initialItem = stringResource(id = R.string.filter_month_all), @@ -50,10 +75,12 @@ fun SwCentralContent( SwCentralNoticeColumn( uiState = uiState, filterState = filterState, + favoriteNotices = favoriteNotices, + refreshing = refreshing, onClickNotice = onClickNotice, onAddToFavorite = onAddToFavorite, onDeleteFromFavorite = onDeleteFromFavorite, - favoriteNotices = favoriteNotices, + onRefresh = onRefresh ) } } @@ -61,37 +88,57 @@ fun SwCentralContent( CircularProgressIndicator() } SwCentralNoticeUiState.Failure -> { - FailureScreen() + FailureScreen(onRefresh = onRefresh) } } } +@OptIn(ExperimentalMaterialApi::class) @Composable fun SwCentralNoticeColumn( uiState: SwCentralNoticeUiState.Success, filterState: NoticeFilterState, favoriteNotices: List, + refreshing: Boolean, onClickNotice: (String) -> Unit, onAddToFavorite: (Notice) -> Unit, - onDeleteFromFavorite: (Notice) -> Unit + onDeleteFromFavorite: (Notice) -> Unit, + onRefresh: () -> Unit ) { - LazyColumn( - Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 18.dp, vertical = 0.dp), - verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.Top) + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = onRefresh + ) + Box( + modifier = Modifier.pullRefresh(pullRefreshState) ) { - items(uiState.notices.filter { filterState.filtering(it) }) { - NoticeCard( - notice = it, - onClickNotice = onClickNotice, - bookmarked = favoriteNotices.contains(it.toFavorite()), - onToggleBookmark = { notice, bookmarked -> - if (bookmarked) { - onAddToFavorite(notice) - } else { - onDeleteFromFavorite(notice) + LazyColumn( + Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 18.dp, vertical = 0.dp), + verticalArrangement = Arrangement.spacedBy(18.dp, Alignment.Top) + ) { + items(uiState.notices.filter { filterState.filtering(it) }) { + NoticeCard( + notice = it, + onClickNotice = onClickNotice, + bookmarked = favoriteNotices.contains(it.toFavorite()), + onToggleBookmark = { notice, bookmarked -> + if (bookmarked) { + onAddToFavorite(notice) + } else { + onDeleteFromFavorite(notice) + } } - }) + ) + } + item { Spacer(modifier = Modifier.height(4.dp)) } } + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + refreshing = refreshing, + state = pullRefreshState, + contentColor = MaterialTheme.colorScheme.primary, + scale = true + ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/osl/OslScreen.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/osl/OslScreen.kt index 186f127..c83a750 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/osl/OslScreen.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/osl/OslScreen.kt @@ -9,7 +9,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler @@ -17,7 +21,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import dev.yjyoon.kwnotice.presentation.R import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeOslBar -import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeTopAppBar import dev.yjyoon.kwnotice.presentation.ui.model.OpenSourceLicense @Composable @@ -31,10 +34,16 @@ fun OslScreen( .fillMaxSize() .background(color = MaterialTheme.colorScheme.background) ) { - KwNoticeTopAppBar( - titleText = stringResource(id = R.string.settings_osl), - actionIcon = Icons.Default.Close, - onActionClick = onClose + TopAppBar( + title = {}, + actions = { + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null + ) + } + } ) LazyColumn( Modifier.padding(horizontal = 18.dp), diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/settings/SettingsScreen.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/settings/SettingsScreen.kt index cadf8c1..6aa581e 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/settings/SettingsScreen.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/settings/SettingsScreen.kt @@ -7,6 +7,8 @@ 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.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.AlertDialog @@ -33,8 +35,8 @@ import dev.yjyoon.kwnotice.domain.model.VersionName import dev.yjyoon.kwnotice.presentation.R import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeDivider import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeLoading +import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeSimpleTopAppBar import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeSwitchBar -import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeTopAppBar import dev.yjyoon.kwnotice.presentation.ui.component.KwNoticeTouchBar import dev.yjyoon.kwnotice.presentation.ui.model.FcmTopicModel import dev.yjyoon.kwnotice.presentation.ui.theme.KwNoticeTheme @@ -103,11 +105,14 @@ fun SettingsContent( versionName: VersionName ) { val uriHandler = LocalUriHandler.current + val scrollState = rememberScrollState() Column( - Modifier.fillMaxSize() + Modifier + .fillMaxSize() + .verticalScroll(scrollState) ) { - KwNoticeTopAppBar( + KwNoticeSimpleTopAppBar( titleText = stringResource(id = R.string.navigation_settings), actionIcon = Icons.Outlined.Info, onActionClick = onOpenDialog @@ -142,6 +147,24 @@ fun SettingsContent( onUnsubscribe = onUnsubscribe ) KwNoticeDivider() + SettingsTitle( + Modifier.padding(horizontal = 16.dp), + text = stringResource(id = R.string.kw_dorm) + ) + Spacer(Modifier.height(4.dp)) + FcmTopicSwitchBar( + uiState = uiState, + fcmTopicModel = FcmTopicModel(FcmTopic.KwDormCommon), + onSubscribe = onSubscribe, + onUnsubscribe = onUnsubscribe + ) + FcmTopicSwitchBar( + uiState = uiState, + fcmTopicModel = FcmTopicModel(FcmTopic.KwDormRecruitment), + onSubscribe = onSubscribe, + onUnsubscribe = onUnsubscribe + ) + KwNoticeDivider() SettingsTitle( Modifier.padding(horizontal = 16.dp), text = stringResource(id = R.string.settings_app_info) diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/splash/SplashActivity.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/splash/SplashActivity.kt index 59656e9..6b126b3 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/splash/SplashActivity.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/splash/SplashActivity.kt @@ -1,9 +1,16 @@ package dev.yjyoon.kwnotice.presentation.ui.splash +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import dev.yjyoon.kwnotice.presentation.R import dev.yjyoon.kwnotice.presentation.ui.base.BaseActivity import dev.yjyoon.kwnotice.presentation.ui.main.MainActivity import kotlinx.coroutines.delay @@ -25,11 +32,48 @@ class SplashActivity : BaseActivity() { } } - private fun handleState(state: SplashState) = + private fun handleState(state: SplashState) { when (state) { - SplashState.Done -> startMainActivity() - SplashState.Waiting -> Unit + SplashState.Done -> requestNotificationPermission() + SplashState.Waiting -> {} } + } + + private fun requestNotificationPermission() { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + Toast.makeText( + this, + R.string.on_disable_notification, + Toast.LENGTH_LONG + ).show() + } + } else { + startMainActivity() + } + } + + private val notificationPermissionLauncher = + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { + if (!it) onDenyNotificationPermission() + startMainActivity() + } + + private fun onDenyNotificationPermission() = + Toast.makeText( + this, + R.string.on_deny_notification_permission, + Toast.LENGTH_LONG + ).show() + private fun startMainActivity() { MainActivity.startActivity(this) diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/theme/Type.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/theme/Type.kt index 1cd56c6..33c2dad 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/theme/Type.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/theme/Type.kt @@ -98,7 +98,7 @@ val KwNoticeTypography = Typography( fontWeight = FontWeight.Bold, letterSpacing = 0.sp, lineHeight = 28.sp, - fontSize = 22.sp + fontSize = 26.sp ), titleMedium = TextStyle( fontFamily = Roboto, diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/util/DateDisplayUtil.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/util/DateDisplayUtil.kt new file mode 100644 index 0000000..d0c3131 --- /dev/null +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/util/DateDisplayUtil.kt @@ -0,0 +1,30 @@ +package dev.yjyoon.kwnotice.presentation.ui.util + +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +object DateDisplayUtil { + fun LocalDate.toRelativeDateString(): String { + val today = LocalDate.now() + + return when { + plusYears(1).isBeforeOrEqual(today) -> { + "${ChronoUnit.YEARS.between(this, today)}년 전" + } + plusMonths(1).isBeforeOrEqual(today) -> { + "${ChronoUnit.MONTHS.between(this, today)}달 전" + } + plusWeeks(1).isBeforeOrEqual(today) -> { + "${ChronoUnit.WEEKS.between(this, today)}주 전" + } + plusDays(1).isBeforeOrEqual(today) -> { + "${ChronoUnit.DAYS.between(this, today)}일 전" + } + else -> { + "오늘" + } + } + } + + private fun LocalDate.isBeforeOrEqual(other: LocalDate) = isEqual(other) || isBefore(other) +} diff --git a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/webview/WebViewScreen.kt b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/webview/WebViewScreen.kt index c2f9f5e..f949ef6 100644 --- a/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/webview/WebViewScreen.kt +++ b/presentation/src/main/java/dev/yjyoon/kwnotice/presentation/ui/webview/WebViewScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -80,6 +81,8 @@ fun WebViewFloatingActionButton( modifier = Modifier.size(28.dp) ) }, - onClick = onClick + onClick = onClick, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary ) } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c693ed3..7e259a6 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -2,13 +2,19 @@ KW 알리미 광운대학교 + 광운대 SW중심대학사업단 + SW중심대 + 빛솔재 공공기숙사 + 빛솔재 공지사항 즐겨찾기 환경설정 - M월 d일 + 공지사항 알림 수신을 위해 알림 권한을 허용해주세요 + 공지사항 알림 수신을 위해 앱 정보에서 알림 권한을 허용해주세요 + 공지사항을 불러올 수 없습니다.\n네트워크 연결 상태를 확인해주세요! 아직 등록된 즐겨찾기가 없습니다.\n공지사항에서 즐겨찾기를 추가해보세요! @@ -21,10 +27,17 @@ 전체 날짜 + 즐겨찾기에서 삭제하였습니다 + 되돌리기 + 웹으로 열기 새로운 공지사항 등록 알림 기존 공지사항 수정 알림 + + 일반 공지사항 등록 알림 + 인원 모집 공지사항 등록 알림 + 앱 정보 개발 정보 yjyoon.dev@gmail.com @@ -33,4 +46,6 @@ "Copyright ⓒ %s All rights reserved." 본 애플리케이션은 광운대학교 재학생들을 위해 재학생이 비영리 목적으로 개발한 공지사항 알림 애플리케이션입니다. ver %s + + Unknown \ No newline at end of file