diff --git a/README.md b/README.md index 094e6dc7..08190dbe 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,127 @@ -# 니차내차 -**너와 나의 차량공유 플랫폼, 니차내차** +

+ + + + + +

+ +

-## 🚗 프로젝트 소개 +
- +## 🚗 너와 나의 차량 공유 플랫폼, 니차내차 + +**안녕하세요!** **저희는** **니차내차를 개발한** **팀 GTA입니다** 😎 + +지금 바로 **내 주변에 있는 차**를 빌릴 수 있어요! + +**상대방과 대화**를 통해 차 대여를 문의해요! + +렌트카가 부담스럽다면? **니차내차**를 이용해볼까요? + +### 📚 문서 +[GTA의 Wiki](https://github.com/boostcampwm-2022/android01-UCMC/wiki) 바로가기 + +[GTA의 프로젝트 소개](https://www.notion.so/boostcamp-wm/Android01-GTA-187240d621664d468243c57fb4387e75) 바로가기 + +
+ +## 🛠 기술 스택 +### 개발 환경 + + +### 라이브러리 + + +
## 👨‍👩‍👧‍👦 멤버 소개 | [김민성](https://www.github.com/minseonglove) | [이동훈](https://www.github.com/ldh019) | [진주영](https://www.github.com/juyoung0520) | [최현지](https://www.github.com/hyunji99Choi) | | :-----: | :-----: | :-----: | :-----: | | K006 | K034 | K053 | K058 | -## 🛠 기술 스택 -## 📚 프로젝트 위키 -[바로가기](https://www.github.com/boostcampwm-2022/android01-UCMC/wiki) +
+ +## 🏗️ 아키텍처 + + + +
+ +## 📱 구성 화면 + +- ### 구글 계정으로 니차내차를 시작하세요! + +

+ + +

+ +
+ +- ### 지도에서 원하는 차를 찾을 수 있어요! + +

+ + +

+ +
+ +- ### 원하는 차를 대여할 수 있어요! 수락될 수도 있지만 거절될 수도 있어요~ + +

+ + +

+ +
+ +- ### 내 차를 등록하고 상세 정보를 수정할 수 있어요! + +

+ + +

+

+ + +

+ +
+ +- ### 차를 반납하면 감사인사를 작성해요! + +

+ + +

+ +
+ +- ### 니차내차 기록을 확인해요! + +

+ + +

+ +
+ +- ### 차주와 이야기를 나눠요! + +

+ +

+ +
+ +- ### 내 정보를 수정해요! + +

+ +

-[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fboostcampwm-2022%2Fandroid01-UCMC&count_bg=%236A94FF&title_bg=%232E2E2E&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) +
diff --git a/build.gradle.kts b/build.gradle.kts index 608217df..a173e190 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ buildscript { classpath(Dependencies.Classpaths.NAVIGATION) classpath(Dependencies.Classpaths.HILT) classpath(Dependencies.Classpaths.CRASHLYTICS) + classpath(Dependencies.Classpaths.JUNIT5) } } diff --git a/buildSrc/src/main/java/com/gta/buildsrc/Configuration.kt b/buildSrc/src/main/java/com/gta/buildsrc/Configuration.kt index 5d3e20f3..6f80ce22 100644 --- a/buildSrc/src/main/java/com/gta/buildsrc/Configuration.kt +++ b/buildSrc/src/main/java/com/gta/buildsrc/Configuration.kt @@ -5,5 +5,5 @@ object Configuration { const val minSdk = 23 const val targetSdk = 33 const val versionCode = 1 - const val versionName = "0.1.0" + const val versionName = "0.2.0" } diff --git a/buildSrc/src/main/java/com/gta/buildsrc/Dependencies.kt b/buildSrc/src/main/java/com/gta/buildsrc/Dependencies.kt index 0df6f70c..3b273da2 100644 --- a/buildSrc/src/main/java/com/gta/buildsrc/Dependencies.kt +++ b/buildSrc/src/main/java/com/gta/buildsrc/Dependencies.kt @@ -49,9 +49,11 @@ object Dependencies { const val LOGGING_INTERCEPTER = "5.0.0-alpha.6" // Test - const val JUNIT = "4.13.2" - const val JUNIT_EXT = "1.1.4" + const val JUNIT5_PLUGIN = "1.8.2.1" + const val JUNIT5 = "5.8.2" const val ESPRESSO_CORE = "3.5.0" + const val MOCKITO = "4.9.0" + const val MOCKITO_KOTLIN = "4.1.0" // Github open Library const val INDICATOR = "4.3" @@ -65,6 +67,8 @@ object Dependencies { const val HILT = "com.google.dagger:hilt-android-gradle-plugin:${Versions.HILT}" const val CRASHLYTICS = "com.google.firebase:firebase-crashlytics-gradle:${Versions.CRASHLYTICS}" + const val JUNIT5 = + "de.mannodermaus.gradle.plugins:android-junit5:${Versions.JUNIT5_PLUGIN}" } object Libraries { @@ -186,10 +190,6 @@ object Dependencies { object Paging { const val PAGING = "androidx.paging:paging-runtime:${Versions.PAGING}" const val KTX = "androidx.paging:paging-common:${Versions.PAGING}" - - fun getAll(): ArrayList { - return arrayListOf(PAGING, KTX) - } } const val INDICATOR = "com.tbuonomo:dotsindicator:${Versions.INDICATOR}" @@ -213,8 +213,11 @@ object Dependencies { const val INJECT = "javax.inject:javax.inject:${Versions.INJECT}" object Test { - const val EXT = "androidx.test.ext:junit:${Versions.JUNIT_EXT}" - const val JUNIT = "junit:junit:${Versions.JUNIT}" + const val JUNIT5_API = "org.junit.jupiter:junit-jupiter-api:${Versions.JUNIT5}" + const val JUNIT5_ENGINE = "org.junit.jupiter:junit-jupiter-engine:${Versions.JUNIT5}" + const val MOCKITO = "org.mockito:mockito-inline:${Versions.MOCKITO}" + const val MOCKITO_KOTLIN = "org.mockito.kotlin:mockito-kotlin:${Versions.MOCKITO_KOTLIN}" + const val MOCKITO_JUPITER = "org.mockito:mockito-junit-jupiter:${Versions.MOCKITO}" } object AndroidTest { @@ -246,6 +249,14 @@ object Dependencies { add(Paging.PAGING) } + val dataTestLibraries = arrayListOf().apply { + add(Test.MOCKITO) + add(Test.MOCKITO_KOTLIN) + add(Test.MOCKITO_JUPITER) + add(Test.JUNIT5_API) + add(Test.JUNIT5_ENGINE) + } + val dataKaptLibraries = arrayListOf().apply { add(Hilt.COMPILER) add(Room.COMPILER) @@ -310,6 +321,12 @@ fun DependencyHandler.testImplementation(list: List) { } } +fun DependencyHandler.testRuntimeOnly(list: List) { + list.forEach { dependency -> + add("testRuntimeOnly", dependency) + } +} + fun DependencyHandler.classpath(list: List) { list.forEach { dependency -> add("classpath", dependency) diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 6f62fedf..8164570a 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("com.google.gms.google-services") kotlin("kapt") id("dagger.hilt.android.plugin") + id("de.mannodermaus.android-junit5") } android { @@ -38,6 +39,9 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + testOptions { + unitTests.isReturnDefaultValues = true + } } dependencies { @@ -48,5 +52,5 @@ dependencies { implementation(Dependencies.Libraries.dataLibraries) kapt(Dependencies.Libraries.dataKaptLibraries) - testImplementation(Dependencies.Libraries.Test.JUNIT) + testImplementation(Dependencies.Libraries.dataTestLibraries) } diff --git a/data/src/main/java/com/gta/data/model/Car.kt b/data/src/main/java/com/gta/data/model/Car.kt index abc02fd6..c21b279a 100644 --- a/data/src/main/java/com/gta/data/model/Car.kt +++ b/data/src/main/java/com/gta/data/model/Car.kt @@ -14,10 +14,10 @@ data class Car( val pinkSlip: PinkSlip = PinkSlip(), val images: List = emptyList(), val price: Int = 10000, - val location: String = "동훈이 집", + val location: String = "정보 없음", val coordinate: Coordinate = Coordinate(), val rentState: String = RentState.UNAVAILABLE.string, - val comment: String = "차였어요", + val comment: String = "정보 없음", val availableDate: AvailableDate = AvailableDate(), val ownerId: String = "정보 없음" ) diff --git a/data/src/main/java/com/gta/data/repository/CarRepositoryImpl.kt b/data/src/main/java/com/gta/data/repository/CarRepositoryImpl.kt index 28ce78dc..adfec3f1 100644 --- a/data/src/main/java/com/gta/data/repository/CarRepositoryImpl.kt +++ b/data/src/main/java/com/gta/data/repository/CarRepositoryImpl.kt @@ -21,14 +21,18 @@ import com.gta.domain.model.RentState import com.gta.domain.model.SimpleCar import com.gta.domain.model.UCMCResult import com.gta.domain.model.UpdateCar +import com.gta.domain.model.UpdateFailException import com.gta.domain.model.UserNotFoundException import com.gta.domain.model.UserProfile import com.gta.domain.repository.CarRepository +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject class CarRepositoryImpl @Inject constructor( @@ -145,17 +149,24 @@ class CarRepositoryImpl @Inject constructor( } } - override fun setCarImagesStorage(carId: String, images: List): Flow> = + override fun setCarImagesStorage( + carId: String, + images: List + ): Flow>> = callbackFlow { - val imageUri = mutableListOf() - images.forEach { img -> - val image = Uri.parse(img) - val name = image.path?.substringAfterLast("/") ?: "" - storageDataSource.uploadPicture( - "car/$carId/${System.currentTimeMillis()}$name", - img - ).first()?.let { uri -> - imageUri.add(uri) + val imageUri = mutableListOf>() + withContext(Dispatchers.IO) { + images.forEach { img -> + launch { + val image = Uri.parse(img) + val name = image.path?.substringAfterLast("/") ?: "" + storageDataSource.uploadPicture( + "car/$carId/${System.currentTimeMillis()}$name", + img + ).first()?.let { uri -> + imageUri.add(UCMCResult.Success(uri)) + } ?: imageUri.add(UCMCResult.Error(UpdateFailException())) + } } } trySend(imageUri) @@ -164,8 +175,12 @@ class CarRepositoryImpl @Inject constructor( override fun deleteImagesStorage(images: List): Flow = callbackFlow { val result = mutableListOf() - images.forEach { img -> - result.add(storageDataSource.deletePicture(img).first()) + withContext(Dispatchers.IO) { + images.forEach { img -> + launch { + result.add(storageDataSource.deletePicture(img).first()) + } + } } trySend(!result.contains(false)) awaitClose() diff --git a/data/src/main/java/com/gta/data/repository/LoginRepositoryImpl.kt b/data/src/main/java/com/gta/data/repository/LoginRepositoryImpl.kt index b08ffc2b..3af20450 100644 --- a/data/src/main/java/com/gta/data/repository/LoginRepositoryImpl.kt +++ b/data/src/main/java/com/gta/data/repository/LoginRepositoryImpl.kt @@ -1,13 +1,12 @@ package com.gta.data.repository +import android.content.res.Resources.NotFoundException import com.gta.data.source.LoginDataSource import com.gta.data.source.MessageTokenDataSource import com.gta.data.source.UserDataSource -import com.gta.domain.model.LoginResult +import com.gta.domain.model.FirestoreException +import com.gta.domain.model.UCMCResult import com.gta.domain.repository.LoginRepository -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.first import javax.inject.Inject @@ -16,27 +15,25 @@ class LoginRepositoryImpl @Inject constructor( private val loginDataSource: LoginDataSource, private val messageTokenDataSource: MessageTokenDataSource ) : LoginRepository { - override fun checkCurrentUser( + override suspend fun checkCurrentUser( uid: String - ): Flow = callbackFlow { + ): UCMCResult { val userInfo = userDataSource.getUser(uid).first() - if (userInfo != null) { - trySend(LoginResult.SUCCESS) + return if (userInfo != null) { + UCMCResult.Success(Unit) } else { - trySend(LoginResult.NEWUSER) + UCMCResult.Error(NotFoundException()) } - awaitClose() } - override fun signUp(uid: String) = callbackFlow { + override suspend fun signUp(uid: String): UCMCResult { val messageToken = messageTokenDataSource.getMessageToken().first() val created = loginDataSource.createUser(uid, messageToken).first() - if (created) { - trySend(LoginResult.SUCCESS) + return if (created) { + UCMCResult.Success(Unit) } else { - trySend(LoginResult.FAILURE) + UCMCResult.Error(FirestoreException()) } - awaitClose() } override suspend fun updateUserMessageToken(uid: String): Boolean { diff --git a/data/src/main/java/com/gta/data/repository/NotificationRepositoryImpl.kt b/data/src/main/java/com/gta/data/repository/NotificationRepositoryImpl.kt index c5bbae09..249a1bfa 100644 --- a/data/src/main/java/com/gta/data/repository/NotificationRepositoryImpl.kt +++ b/data/src/main/java/com/gta/data/repository/NotificationRepositoryImpl.kt @@ -42,7 +42,7 @@ class NotificationRepositoryImpl @Inject constructor( suspend fun getNotificationInfoDetailItem(notifyInfo: NotificationInfo): NotificationInfo { val reservation = reservationDataSource.getReservation(notifyInfo.reservationId).first() val from = notifyInfo.fromId - val car = reservation?.carId ?: "-" + val car = reservation?.carId ?: "정보 없음" withContext(Dispatchers.IO) { launch { diff --git a/data/src/main/java/com/gta/data/repository/ReportRepositoryImpl.kt b/data/src/main/java/com/gta/data/repository/ReportRepositoryImpl.kt index 8dd19b42..f4d4c891 100644 --- a/data/src/main/java/com/gta/data/repository/ReportRepositoryImpl.kt +++ b/data/src/main/java/com/gta/data/repository/ReportRepositoryImpl.kt @@ -14,20 +14,23 @@ class ReportRepositoryImpl @Inject constructor( private var lastReportedTime = 0L - override suspend fun reportUser(uid: String): UCMCResult { - val cooldown = REPORT_COOL_DOWN - getTimeAfterReporting() + override suspend fun reportUser( + uid: String, + currentTime: Long + ): UCMCResult { + val cooldown = REPORT_COOL_DOWN - getTimeAfterReporting(currentTime) return if (cooldown > 0) { UCMCResult.Error(CoolDownException(cooldown / 1000 + 1)) } else { - addReportCount(uid) + addReportCount(uid, currentTime) } } - private suspend fun addReportCount(uid: String): UCMCResult { + private suspend fun addReportCount(uid: String, currentTime: Long): UCMCResult { return userDataSource.getUser(uid).first()?.let { user -> val result = userDataSource.addReportCount(uid, user.reportCount + 1).first() if (result) { - lastReportedTime = System.currentTimeMillis() + lastReportedTime = currentTime UCMCResult.Success(Unit) } else { UCMCResult.Error(FirestoreException()) @@ -35,8 +38,8 @@ class ReportRepositoryImpl @Inject constructor( } ?: UCMCResult.Error(FirestoreException()) } - private fun getTimeAfterReporting(): Long = - (System.currentTimeMillis() - lastReportedTime) + private fun getTimeAfterReporting(currentTime: Long): Long = + (currentTime - lastReportedTime) companion object { private const val REPORT_COOL_DOWN = 10000 diff --git a/data/src/main/java/com/gta/data/repository/TransactionRepositoryImpl.kt b/data/src/main/java/com/gta/data/repository/TransactionRepositoryImpl.kt index 70a948e6..d379af89 100644 --- a/data/src/main/java/com/gta/data/repository/TransactionRepositoryImpl.kt +++ b/data/src/main/java/com/gta/data/repository/TransactionRepositoryImpl.kt @@ -1,17 +1,19 @@ package com.gta.data.repository +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData import com.gta.data.source.TransactionDataSource +import com.gta.data.source.TransactionPagingSource import com.gta.domain.model.SimpleReservation import com.gta.domain.repository.TransactionRepository -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.Flow import javax.inject.Inject class TransactionRepositoryImpl @Inject constructor(private val transactionDataSource: TransactionDataSource) : TransactionRepository { - override suspend fun getYourCarTransactions(uid: String): List { - return transactionDataSource.getYourCarTransactions(uid).first() - } - - override suspend fun getMyCarTransactions(uid: String): List { - return transactionDataSource.getMyCarTransactions(uid).first() + override fun getTransactions(userId: String, isLender: Boolean): Flow> { + return Pager(PagingConfig(transactionDataSource.pagingSize.toInt())) { + TransactionPagingSource(userId, isLender, transactionDataSource) + }.flow } } diff --git a/data/src/main/java/com/gta/data/source/NotificationPagingSource.kt b/data/src/main/java/com/gta/data/source/NotificationPagingSource.kt index a4d985ce..f450b605 100644 --- a/data/src/main/java/com/gta/data/source/NotificationPagingSource.kt +++ b/data/src/main/java/com/gta/data/source/NotificationPagingSource.kt @@ -19,8 +19,14 @@ class NotificationPagingSource( override suspend fun load(params: LoadParams): LoadResult { return try { val currentPage = params.key ?: dataSource.getNotificationInfoCurrentItem(userId) - val lastDocumentSnapshot = currentPage.documents[currentPage.size() - 1] - val nextPage = dataSource.getNotificationInfoNextItem(userId, lastDocumentSnapshot) + + val nextPage: QuerySnapshot? = + if (currentPage.size() > 0) { + val lastDocumentSnapshot = currentPage.documents[currentPage.size() - 1] + dataSource.getNotificationInfoNextItem(userId, lastDocumentSnapshot) + } else { + null + } LoadResult.Page( data = currentPage.map { diff --git a/data/src/main/java/com/gta/data/source/TransactionDataSource.kt b/data/src/main/java/com/gta/data/source/TransactionDataSource.kt index e2d36bf4..4ab49f21 100644 --- a/data/src/main/java/com/gta/data/source/TransactionDataSource.kt +++ b/data/src/main/java/com/gta/data/source/TransactionDataSource.kt @@ -1,43 +1,51 @@ package com.gta.data.source +import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FirebaseFirestore -import com.gta.domain.model.Reservation -import com.gta.domain.model.SimpleReservation -import com.gta.domain.model.toSimpleReservation -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import com.google.firebase.firestore.QuerySnapshot +import kotlinx.coroutines.tasks.await import javax.inject.Inject class TransactionDataSource @Inject constructor(private val fireStore: FirebaseFirestore) { + val pagingSize = 10L - fun getYourCarTransactions(uid: String): Flow> = callbackFlow { - fireStore.collection("reservations").whereEqualTo("lenderId", uid).get().addOnCompleteListener { - if (it.isSuccessful) { - it.result.map { snapshot -> - snapshot.toObject(Reservation::class.java).toSimpleReservation(snapshot.id) - }.also { result -> - trySend(result) - } - } else { - trySend(emptyList()) - } - } - awaitClose() + suspend fun getYourCarTransactions(uid: String): QuerySnapshot { + return fireStore.collection("reservations") + .whereEqualTo("lenderId", uid) + .limit(pagingSize) + .get() + .await() } - fun getMyCarTransactions(uid: String): Flow> = callbackFlow { - fireStore.collection("reservations").whereEqualTo("ownerId", uid).get().addOnCompleteListener { - if (it.isSuccessful) { - it.result.map { snapshot -> - snapshot.toObject(Reservation::class.java).toSimpleReservation(snapshot.id) - }.also { result -> - trySend(result) - } - } else { - trySend(emptyList()) - } - } - awaitClose() + suspend fun getYourCarTransactionsFromCursor( + uid: String, + docCursor: DocumentSnapshot + ): QuerySnapshot { + return fireStore.collection("reservations") + .whereEqualTo("lenderId", uid) + .limit(pagingSize) + .startAfter(docCursor) + .get() + .await() + } + + suspend fun getMyCarTransactions(uid: String): QuerySnapshot { + return fireStore.collection("reservations") + .whereEqualTo("ownerId", uid) + .limit(pagingSize) + .get() + .await() + } + + suspend fun getMyCarTransactionsFromCursor( + uid: String, + docCursor: DocumentSnapshot + ): QuerySnapshot { + return fireStore.collection("reservations") + .whereEqualTo("ownerId", uid) + .limit(pagingSize) + .startAfter(docCursor) + .get() + .await() } } diff --git a/data/src/main/java/com/gta/data/source/TransactionPagingSource.kt b/data/src/main/java/com/gta/data/source/TransactionPagingSource.kt new file mode 100644 index 00000000..5dcfd42a --- /dev/null +++ b/data/src/main/java/com/gta/data/source/TransactionPagingSource.kt @@ -0,0 +1,50 @@ +package com.gta.data.source + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.QuerySnapshot +import com.gta.domain.model.Reservation +import com.gta.domain.model.SimpleReservation +import com.gta.domain.model.toSimpleReservation +import kotlin.reflect.KSuspendFunction1 +import kotlin.reflect.KSuspendFunction2 + +class TransactionPagingSource( + private val userId: String, + isLender: Boolean, + dataSource: TransactionDataSource +) : PagingSource() { + + private val getTransactions: KSuspendFunction1 = + if (isLender) dataSource::getYourCarTransactions else dataSource::getMyCarTransactions + + private val getTransactionsNext: KSuspendFunction2 = + if (isLender) dataSource::getYourCarTransactionsFromCursor else dataSource::getMyCarTransactionsFromCursor + + override fun getRefreshKey(state: PagingState): QuerySnapshot? = + null + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val currentPage = params.key ?: getTransactions(userId) + val nextPage: QuerySnapshot? = + if (currentPage.size() > 0) { + val lastDocumentSnapshot = currentPage.documents[currentPage.size() - 1] + getTransactionsNext(userId, lastDocumentSnapshot) + } else { + null + } + + LoadResult.Page( + data = currentPage.map { snapshot -> + snapshot.toObject(Reservation::class.java).toSimpleReservation(snapshot.id) + }, + prevKey = null, + nextKey = nextPage + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} diff --git a/data/src/test/java/com/gta/data/ExampleUnitTest.kt b/data/src/test/java/com/gta/data/ExampleUnitTest.kt deleted file mode 100644 index 5ad9c990..00000000 --- a/data/src/test/java/com/gta/data/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.gta.data - -import org.junit.Assert.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/data/src/test/java/com/gta/data/ReportUnitTest.kt b/data/src/test/java/com/gta/data/ReportUnitTest.kt new file mode 100644 index 00000000..093bda0f --- /dev/null +++ b/data/src/test/java/com/gta/data/ReportUnitTest.kt @@ -0,0 +1,89 @@ +package com.gta.data + +import com.gta.data.model.UserInfo +import com.gta.data.repository.ReportRepositoryImpl +import com.gta.data.source.UserDataSource +import com.gta.domain.model.CoolDownException +import com.gta.domain.model.FirestoreException +import com.gta.domain.model.UCMCResult +import com.gta.domain.repository.ReportRepository +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.eq + +@ExtendWith(MockitoExtension::class) +class ReportUnitTest( + @Mock private val userDataSource: UserDataSource +) { + + private val repository: ReportRepository = ReportRepositoryImpl(userDataSource) + + @BeforeEach + fun init() { + `when`(userDataSource.addReportCount(eq(GOOD_UID), anyInt())).thenReturn(flow { emit(true) }) + `when`(userDataSource.addReportCount(eq(BAD_UID), anyInt())).thenReturn(flow { emit(false) }) + `when`(userDataSource.getUser(GOOD_UID)).thenReturn(flow { emit(UserInfo()) }) + `when`(userDataSource.getUser(BAD_UID)).thenReturn(flow { emit(null) }) + } + + @Test + @DisplayName("reportUser : 유효한 uid를 매개변수로 받으면 Success(Unit)을 리턴한다.") + fun Should_Success_When_Gooduid() { + runBlocking { + Assertions.assertEquals(UCMCResult.Success(Unit), repository.reportUser(GOOD_UID)) + } + } + + @Test + @DisplayName("reportUser : 유효하지 않은 uid를 매개변수로 받으면 Error(FirestoreException)을 리턴한다.") + fun Should_FirestoreException_When_Baduid() { + runBlocking { + val result = repository.reportUser(BAD_UID) + Assertions.assertTrue(result is UCMCResult.Error && result.e is FirestoreException) + } + } + + @Test + @DisplayName("reportUser : 메소드를 호출해서 Success(Unit)을 리턴받고 10초안에 다시 호출하면 Error(CoolDownException)을 리턴한다.") + fun Should_Success_When_SuccessAndCallIn10seconds() { + runBlocking { + Assertions.assertEquals(UCMCResult.Success(Unit), repository.reportUser(GOOD_UID)) + val result = repository.reportUser(GOOD_UID) + Assertions.assertTrue(result is UCMCResult.Error && result.e is CoolDownException) + } + } + + @Test + @DisplayName("reportUser : 메소드를 호출해서 Error를 리턴받으면 10초의 대기시간이 적용되지 않는다.") + fun Should_Success_When_ErrorAndCallIn10seconds() { + runBlocking { + val result = repository.reportUser(BAD_UID) + Assertions.assertTrue(result is UCMCResult.Error && result.e is FirestoreException) + Assertions.assertEquals(UCMCResult.Success(Unit), repository.reportUser(GOOD_UID)) + } + } + + @Test + @DisplayName("reportUser : 메소드를 호출한 뒤 10초뒤에 다시 호출하면 Success(Unit)을 리턴한다.") + fun Should_Success_When_SuccessAndCallAfter10seconds() { + runBlocking { + val currentTime = System.currentTimeMillis() + Assertions.assertEquals(UCMCResult.Success(Unit), repository.reportUser(GOOD_UID, currentTime)) + Assertions.assertEquals(UCMCResult.Success(Unit), repository.reportUser(GOOD_UID, currentTime + 10000)) + } + } + + companion object { + private const val GOOD_UID = "GoodDonghoon" + private const val BAD_UID = "BadDonghoon" + } +} diff --git a/domain/src/main/java/com/gta/domain/model/Coordinate.kt b/domain/src/main/java/com/gta/domain/model/Coordinate.kt index c42a9b13..69cdea7f 100644 --- a/domain/src/main/java/com/gta/domain/model/Coordinate.kt +++ b/domain/src/main/java/com/gta/domain/model/Coordinate.kt @@ -1,6 +1,6 @@ package com.gta.domain.model data class Coordinate( - val latitude: Double = 0.0, - val longitude: Double = 0.0 + val latitude: Double = 37.3588798, + val longitude: Double = 127.1051933 ) : java.io.Serializable diff --git a/domain/src/main/java/com/gta/domain/model/LoginResult.kt b/domain/src/main/java/com/gta/domain/model/LoginResult.kt deleted file mode 100644 index 8af43cd0..00000000 --- a/domain/src/main/java/com/gta/domain/model/LoginResult.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.gta.domain.model - -enum class LoginResult { - SUCCESS, FAILURE, NEWUSER -} diff --git a/domain/src/main/java/com/gta/domain/model/Notification.kt b/domain/src/main/java/com/gta/domain/model/Notification.kt index 2d2fd415..a63a2123 100644 --- a/domain/src/main/java/com/gta/domain/model/Notification.kt +++ b/domain/src/main/java/com/gta/domain/model/Notification.kt @@ -4,7 +4,8 @@ data class Notification( val type: String = "", val message: String = "", val reservationId: String = "정보 없음", - val fromId: String = "정보 없음" + val fromId: String = "정보 없음", + val timestamp: Long = 0 ) enum class NotificationType(val title: String, val msg: String) { diff --git a/domain/src/main/java/com/gta/domain/model/UCMCException.kt b/domain/src/main/java/com/gta/domain/model/UCMCException.kt index 5e5552cf..1c01d2d2 100644 --- a/domain/src/main/java/com/gta/domain/model/UCMCException.kt +++ b/domain/src/main/java/com/gta/domain/model/UCMCException.kt @@ -6,3 +6,4 @@ class DuplicatedItemException : Exception() class DeleteFailException : Exception() class UserNotFoundException : Exception() class ExpiredItemException : Exception() +class UpdateFailException : Exception() diff --git a/domain/src/main/java/com/gta/domain/repository/CarRepository.kt b/domain/src/main/java/com/gta/domain/repository/CarRepository.kt index 1a946cde..0339e87f 100644 --- a/domain/src/main/java/com/gta/domain/repository/CarRepository.kt +++ b/domain/src/main/java/com/gta/domain/repository/CarRepository.kt @@ -20,6 +20,6 @@ interface CarRepository { fun getAllCars(): Flow> fun getNearCars(min: Coordinate, max: Coordinate): Flow>> suspend fun removeCar(userId: String, carId: String): UCMCResult - fun setCarImagesStorage(carId: String, images: List): Flow> + fun setCarImagesStorage(carId: String, images: List): Flow>> fun deleteImagesStorage(images: List): Flow } diff --git a/domain/src/main/java/com/gta/domain/repository/LoginRepository.kt b/domain/src/main/java/com/gta/domain/repository/LoginRepository.kt index 95ee3f0d..8b6dfcb1 100644 --- a/domain/src/main/java/com/gta/domain/repository/LoginRepository.kt +++ b/domain/src/main/java/com/gta/domain/repository/LoginRepository.kt @@ -1,10 +1,9 @@ package com.gta.domain.repository -import com.gta.domain.model.LoginResult -import kotlinx.coroutines.flow.Flow +import com.gta.domain.model.UCMCResult interface LoginRepository { - fun checkCurrentUser(uid: String): Flow - fun signUp(uid: String): Flow + suspend fun checkCurrentUser(uid: String): UCMCResult + suspend fun signUp(uid: String): UCMCResult suspend fun updateUserMessageToken(uid: String): Boolean } diff --git a/domain/src/main/java/com/gta/domain/repository/ReportRepository.kt b/domain/src/main/java/com/gta/domain/repository/ReportRepository.kt index 592b3cf8..9c2fe461 100644 --- a/domain/src/main/java/com/gta/domain/repository/ReportRepository.kt +++ b/domain/src/main/java/com/gta/domain/repository/ReportRepository.kt @@ -3,5 +3,8 @@ package com.gta.domain.repository import com.gta.domain.model.UCMCResult interface ReportRepository { - suspend fun reportUser(uid: String): UCMCResult + suspend fun reportUser( + uid: String, + currentTime: Long = System.currentTimeMillis() + ): UCMCResult } diff --git a/domain/src/main/java/com/gta/domain/repository/TransactionRepository.kt b/domain/src/main/java/com/gta/domain/repository/TransactionRepository.kt index 5b878450..80dcf081 100644 --- a/domain/src/main/java/com/gta/domain/repository/TransactionRepository.kt +++ b/domain/src/main/java/com/gta/domain/repository/TransactionRepository.kt @@ -1,8 +1,9 @@ package com.gta.domain.repository +import androidx.paging.PagingData import com.gta.domain.model.SimpleReservation +import kotlinx.coroutines.flow.Flow interface TransactionRepository { - suspend fun getYourCarTransactions(uid: String): List - suspend fun getMyCarTransactions(uid: String): List + fun getTransactions(userId: String, isLender: Boolean): Flow> } diff --git a/domain/src/main/java/com/gta/domain/usecase/cardetail/SetStateAtCarDetailUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/cardetail/SetStateAtCarDetailUseCase.kt deleted file mode 100644 index 5fdcc417..00000000 --- a/domain/src/main/java/com/gta/domain/usecase/cardetail/SetStateAtCarDetailUseCase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.gta.domain.usecase.cardetail - -import com.gta.domain.repository.CarRepository -import javax.inject.Inject - -class SetStateAtCarDetailUseCase @Inject constructor( - private val carRepository: CarRepository -) diff --git a/domain/src/main/java/com/gta/domain/usecase/cardetail/edit/SetCarImagesUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/cardetail/edit/SetCarImagesUseCase.kt index 8eb6ce84..f503bac5 100644 --- a/domain/src/main/java/com/gta/domain/usecase/cardetail/edit/SetCarImagesUseCase.kt +++ b/domain/src/main/java/com/gta/domain/usecase/cardetail/edit/SetCarImagesUseCase.kt @@ -1,5 +1,6 @@ package com.gta.domain.usecase.cardetail.edit +import com.gta.domain.model.UCMCResult import com.gta.domain.repository.CarRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -7,7 +8,7 @@ import javax.inject.Inject class SetCarImagesUseCase @Inject constructor( private val carRepository: CarRepository ) { - operator fun invoke(carId: String, images: List): Flow> { + operator fun invoke(carId: String, images: List): Flow>> { return carRepository.setCarImagesStorage(carId, images) } } diff --git a/domain/src/main/java/com/gta/domain/usecase/cardetail/edit/UploadCarImagesUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/cardetail/edit/UploadCarImagesUseCase.kt index d2685322..a040e507 100644 --- a/domain/src/main/java/com/gta/domain/usecase/cardetail/edit/UploadCarImagesUseCase.kt +++ b/domain/src/main/java/com/gta/domain/usecase/cardetail/edit/UploadCarImagesUseCase.kt @@ -1,26 +1,34 @@ package com.gta.domain.usecase.cardetail.edit +import com.gta.domain.model.DeleteFailException +import com.gta.domain.model.UCMCResult import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import javax.inject.Inject class UploadCarImagesUseCase @Inject constructor( private val deleteImagesUseCase: DeleteImagesUseCase, private val setCarImagesUseCase: SetCarImagesUseCase ) { - operator fun invoke(carId: String, old: List, new: List): Flow> { - val delete = old.filter { !new.contains(it) } - val update = new.filter { !old.contains(it) } + operator fun invoke( + carId: String, + oldData: List, + newData: List + ): Flow>> { + val deleteData = oldData.filter { !newData.contains(it) } + val updateData = newData.filter { !oldData.contains(it) } /* 1. 삭제된 이미지는 저장소에서 삭제 2. 새로 추가된 이미지만 저장소에 새로 추가 3. 기존에 있던 이미지 + 새로 추가된 이미지 주소값 return */ - return deleteImagesUseCase(delete).flatMapLatest { - setCarImagesUseCase(carId, update).map { - old.filter { o -> new.contains(o) } + it + + return deleteImagesUseCase(deleteData).combine(setCarImagesUseCase(carId, updateData)) { deleteResult, list -> + if (!deleteResult) { + oldData.filter { o -> newData.contains(o) }.map { UCMCResult.Success(it) } + list + UCMCResult.Error(DeleteFailException()) + } else { + oldData.filter { o -> newData.contains(o) }.map { UCMCResult.Success(it) } + list } } } diff --git a/domain/src/main/java/com/gta/domain/usecase/login/CheckCurrentUserUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/login/CheckCurrentUserUseCase.kt index 19019d5e..f9b4a6c0 100644 --- a/domain/src/main/java/com/gta/domain/usecase/login/CheckCurrentUserUseCase.kt +++ b/domain/src/main/java/com/gta/domain/usecase/login/CheckCurrentUserUseCase.kt @@ -1,18 +1,12 @@ package com.gta.domain.usecase.login -import com.gta.domain.model.LoginResult +import com.gta.domain.model.UCMCResult import com.gta.domain.repository.LoginRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class CheckCurrentUserUseCase @Inject constructor( - private val repository: LoginRepository, - private val updateUserMessageTokenUseCase: UpdateUserMessageTokenUseCase + private val repository: LoginRepository ) { - suspend operator fun invoke(uid: String, shouldUpdateMessageToken: Boolean = false): Flow { - if (shouldUpdateMessageToken) { - updateUserMessageTokenUseCase(uid) - } - return repository.checkCurrentUser(uid) - } + suspend operator fun invoke(uid: String): UCMCResult = + repository.checkCurrentUser(uid) } diff --git a/domain/src/main/java/com/gta/domain/usecase/login/SignUpUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/login/SignUpUseCase.kt index 8c1dc14e..d590c9a5 100644 --- a/domain/src/main/java/com/gta/domain/usecase/login/SignUpUseCase.kt +++ b/domain/src/main/java/com/gta/domain/usecase/login/SignUpUseCase.kt @@ -1,12 +1,11 @@ package com.gta.domain.usecase.login -import com.gta.domain.model.LoginResult +import com.gta.domain.model.UCMCResult import com.gta.domain.repository.LoginRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class SignUpUseCase @Inject constructor( private val repository: LoginRepository ) { - operator fun invoke(uid: String): Flow = repository.signUp(uid) + suspend operator fun invoke(uid: String): UCMCResult = repository.signUp(uid) } diff --git a/domain/src/main/java/com/gta/domain/usecase/reservation/CreateReservationUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/reservation/CreateReservationUseCase.kt index 036dbb57..4b73365d 100644 --- a/domain/src/main/java/com/gta/domain/usecase/reservation/CreateReservationUseCase.kt +++ b/domain/src/main/java/com/gta/domain/usecase/reservation/CreateReservationUseCase.kt @@ -19,7 +19,8 @@ class CreateReservationUseCase @Inject constructor( type = NotificationType.REQUEST_RESERVATION.title, message = NotificationType.REQUEST_RESERVATION.msg, reservationId = reservationId, - fromId = reservation.lenderId + fromId = reservation.lenderId, + timestamp = System.currentTimeMillis() ), reservation.ownerId ) diff --git a/domain/src/main/java/com/gta/domain/usecase/reservation/FinishReservationUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/reservation/FinishReservationUseCase.kt index 5ade1b0f..345d9c74 100644 --- a/domain/src/main/java/com/gta/domain/usecase/reservation/FinishReservationUseCase.kt +++ b/domain/src/main/java/com/gta/domain/usecase/reservation/FinishReservationUseCase.kt @@ -23,7 +23,8 @@ class FinishReservationUseCase @Inject constructor( type = if (accepted) NotificationType.ACCEPT_RESERVATION.title else NotificationType.DECLINE_RESERVATION.title, message = if (accepted) NotificationType.ACCEPT_RESERVATION.msg else NotificationType.DECLINE_RESERVATION.msg, reservationId = reservationId, - fromId = ownerId + fromId = ownerId, + timestamp = System.currentTimeMillis() ), reservation.lenderId ) diff --git a/domain/src/main/java/com/gta/domain/usecase/returncar/ReturnCarUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/returncar/ReturnCarUseCase.kt index 50ed58e7..00941885 100644 --- a/domain/src/main/java/com/gta/domain/usecase/returncar/ReturnCarUseCase.kt +++ b/domain/src/main/java/com/gta/domain/usecase/returncar/ReturnCarUseCase.kt @@ -20,7 +20,8 @@ class ReturnCarUseCase @Inject constructor( type = NotificationType.RETURN_CAR.title, message = NotificationType.RETURN_CAR.msg, reservationId = reservationId, - fromId = userId + fromId = userId, + timestamp = System.currentTimeMillis() ) return ownerId.isNotEmpty() && reservationRepository.updateReservationState( reservationId, diff --git a/domain/src/main/java/com/gta/domain/usecase/transaction/GetTransactionsUseCase.kt b/domain/src/main/java/com/gta/domain/usecase/transaction/GetTransactionsUseCase.kt index 18244d90..0bc64bbb 100644 --- a/domain/src/main/java/com/gta/domain/usecase/transaction/GetTransactionsUseCase.kt +++ b/domain/src/main/java/com/gta/domain/usecase/transaction/GetTransactionsUseCase.kt @@ -1,10 +1,15 @@ package com.gta.domain.usecase.transaction +import androidx.paging.PagingData +import androidx.paging.filter +import androidx.paging.map import com.gta.domain.model.Transaction import com.gta.domain.model.toTransaction import com.gta.domain.repository.CarRepository import com.gta.domain.repository.TransactionRepository +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import javax.inject.Inject class GetTransactionsUseCase @Inject constructor( @@ -16,22 +21,20 @@ class GetTransactionsUseCase @Inject constructor( private val completedCondition = { transaction: Transaction -> transaction.reservationState.state < 0 } - suspend operator fun invoke( + operator fun invoke( uid: String, isLender: Boolean, isTrading: Boolean - ): List { - val transactions = if (isLender) { - transactionRepository.getYourCarTransactions(uid) - } else { - transactionRepository.getMyCarTransactions(uid) - } + ): Flow> { + val transactionPagingData = transactionRepository.getTransactions(uid, isLender) val filterCondition = if (isTrading) tradingCondition else completedCondition - return transactions.map { reservation -> - val simpleCar = carRepository.getSimpleCar(reservation.carId).first() - reservation.toTransaction(simpleCar) - }.filter(filterCondition) + return transactionPagingData.map { pagingData -> + pagingData.map { reservation -> + val simpleCar = carRepository.getSimpleCar(reservation.carId).first() + reservation.toTransaction(simpleCar) + }.filter(filterCondition) + } } } diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 602d9a55..cf62bdb8 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -53,7 +53,5 @@ dependencies { implementation(Dependencies.Libraries.presentationLibraries) kapt(Dependencies.Libraries.presentationKaptLibraries) - testImplementation(Dependencies.Libraries.Test.JUNIT) - androidTestImplementation(Dependencies.Libraries.Test.EXT) androidTestImplementation(Dependencies.Libraries.AndroidTest.ESPRESSO_CORE) } diff --git a/presentation/src/main/java/com/gta/presentation/NotificationService.kt b/presentation/src/main/java/com/gta/presentation/NotificationService.kt index 004196e8..5ccad879 100644 --- a/presentation/src/main/java/com/gta/presentation/NotificationService.kt +++ b/presentation/src/main/java/com/gta/presentation/NotificationService.kt @@ -65,6 +65,7 @@ class NotificationService : FirebaseMessagingService() { .setContentText(message.data["message"]) .setContentIntent(createPendingIntent(message)) .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) notificationManager.notify(0, builder.build()) } diff --git a/presentation/src/main/java/com/gta/presentation/ui/BindingAdapter.kt b/presentation/src/main/java/com/gta/presentation/ui/BindingAdapter.kt index bd932651..858c756b 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/BindingAdapter.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/BindingAdapter.kt @@ -4,8 +4,6 @@ import android.widget.Button import android.widget.ImageView import android.widget.TextView import androidx.databinding.BindingAdapter -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView import com.gta.domain.model.AvailableDate import com.gta.domain.model.NotificationType import com.gta.domain.model.ReservationState @@ -70,7 +68,7 @@ fun setReservationTime(textView: TextView, selection: AvailableDate?, dateType: @BindingAdapter("text_AvailableDate") fun setAvailableDateText(textView: TextView, availableDate: AvailableDate?) { - textView.text = if (availableDate != null) { + textView.text = if (availableDate != null && !(availableDate.start == 0L && availableDate.end == 0L)) { String.format( textView.resources.getString(R.string.car_edit_rent_available_day_format), DateUtil.dateFormat.format(availableDate.start), @@ -149,10 +147,3 @@ fun setReservationState(textView: TextView, reservationState: ReservationState) } } } - -@BindingAdapter("submitList") -fun bindSubmitList(view: RecyclerView, itemList: List?) { - view.adapter?.let { - (view.adapter as ListAdapter).submitList(itemList) - } -} diff --git a/presentation/src/main/java/com/gta/presentation/ui/MainActivity.kt b/presentation/src/main/java/com/gta/presentation/ui/MainActivity.kt index b9d78db2..57997b3d 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/MainActivity.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/MainActivity.kt @@ -11,6 +11,7 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController +import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.gta.presentation.R import com.gta.presentation.databinding.ActivityMainBinding import com.gta.presentation.secret.NAVER_MAP_CLIENT_ID @@ -20,10 +21,14 @@ import com.gta.presentation.util.repeatOnStarted import com.naver.maps.map.NaverMapSdk import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest +import javax.inject.Inject @AndroidEntryPoint class MainActivity : BaseActivity(ActivityMainBinding::inflate) { + @Inject + lateinit var googleSignInClient: GoogleSignInClient + private val viewModel: MainViewModel by viewModels() private val navHostFragment by lazy { supportFragmentManager.findFragmentById(R.id.fcv_main) as NavHostFragment } @@ -44,7 +49,9 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl repeatOnStarted(this) { viewModel.changeAuthStateEvent.collectLatest { state -> if (state) { - startLoginActivity() + googleSignInClient.signOut().addOnCompleteListener { + startLoginActivity() + } } } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/MainViewModel.kt b/presentation/src/main/java/com/gta/presentation/ui/MainViewModel.kt index a13984a4..52468d10 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/MainViewModel.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/MainViewModel.kt @@ -3,6 +3,8 @@ package com.gta.presentation.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.firebase.auth.FirebaseAuth +import com.gta.domain.usecase.login.UpdateUserMessageTokenUseCase +import com.gta.presentation.util.FirebaseUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -11,7 +13,8 @@ import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - private val auth: FirebaseAuth + private val auth: FirebaseAuth, + private val updateUserMessageTokenUseCase: UpdateUserMessageTokenUseCase ) : ViewModel() { private val _changeAuthStateEvent = MutableSharedFlow() @@ -27,6 +30,13 @@ class MainViewModel @Inject constructor( init { auth.addAuthStateListener(authStateListener) + updateMessageToken() + } + + private fun updateMessageToken() { + viewModelScope.launch { + updateUserMessageTokenUseCase(FirebaseUtil.uid) + } } override fun onCleared() { diff --git a/presentation/src/main/java/com/gta/presentation/ui/cardetail/CarDetailFragment.kt b/presentation/src/main/java/com/gta/presentation/ui/cardetail/CarDetailFragment.kt index 63535adf..fcd2895c 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/cardetail/CarDetailFragment.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/cardetail/CarDetailFragment.kt @@ -14,6 +14,7 @@ import com.gta.presentation.R import com.gta.presentation.databinding.FragmentCarDetailBinding import com.gta.presentation.ui.MainActivity import com.gta.presentation.ui.base.BaseFragment +import com.gta.presentation.util.FirebaseUtil import com.gta.presentation.util.repeatOnStarted import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest @@ -47,6 +48,11 @@ class CarDetailFragment : BaseFragment( viewModel.carInfo.collectLatest { (requireActivity() as MainActivity).supportActionBar?.title = it.licensePlate pagerAdapter.submitList(it.images) + if (it.owner.id == FirebaseUtil.uid) { + binding.tvReportPost.visibility = View.GONE + binding.inOwnerProfile.tvChatting.visibility = View.GONE + binding.inOwnerProfile.tvReport.visibility = View.GONE + } } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/cardetail/OwnerProfileFragment.kt b/presentation/src/main/java/com/gta/presentation/ui/cardetail/OwnerProfileFragment.kt index c67b4433..c411af48 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/cardetail/OwnerProfileFragment.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/cardetail/OwnerProfileFragment.kt @@ -10,6 +10,7 @@ import com.gta.domain.model.UCMCResult import com.gta.presentation.R import com.gta.presentation.databinding.FragmentOwnerProfileBinding import com.gta.presentation.ui.base.BaseFragment +import com.gta.presentation.util.FirebaseUtil import com.gta.presentation.util.repeatOnStarted import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest @@ -40,6 +41,14 @@ class OwnerProfileFragment : BaseFragment( ) } + repeatOnStarted(viewLifecycleOwner) { + viewModel.owner.collectLatest { + if (it.id == FirebaseUtil.uid) { + binding.tvReport.visibility = View.GONE + } + } + } + repeatOnStarted(viewLifecycleOwner) { viewModel.reportEvent.collectLatest { result -> when (result) { diff --git a/presentation/src/main/java/com/gta/presentation/ui/cardetail/edit/CarEditFragment.kt b/presentation/src/main/java/com/gta/presentation/ui/cardetail/edit/CarEditFragment.kt index e5472c4e..9d2fa36f 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/cardetail/edit/CarEditFragment.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/cardetail/edit/CarEditFragment.kt @@ -17,13 +17,16 @@ import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.DateValidatorPointForward import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.snackbar.Snackbar +import com.gta.domain.model.DeleteFailException +import com.gta.domain.model.UCMCResult +import com.gta.domain.model.UpdateFailException import com.gta.presentation.R import com.gta.presentation.databinding.FragmentCarEditBinding import com.gta.presentation.ui.base.BaseFragment +import com.gta.presentation.util.DateUtil import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import timber.log.Timber import java.text.DecimalFormat @AndroidEntryPoint @@ -60,7 +63,7 @@ class CarEditFragment : BaseFragment( private val constraints: CalendarConstraints by lazy { CalendarConstraints.Builder() .setValidator(DateValidatorPointForward.now()) - .setEnd(MaterialDatePicker.thisMonthInUtcMilliseconds()) + .setEnd(MaterialDatePicker.thisMonthInUtcMilliseconds() + DateUtil.DAY_TIME_UNIT * 31) .build() } private val datePicker by lazy { @@ -168,13 +171,22 @@ class CarEditFragment : BaseFragment( } } - binding.etPrice.setOnFocusChangeListener { _, boolean -> + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.price.collectLatest { + if (it.isNotEmpty()) { + binding.tvPrice.text = decimalFormat.format(it.toInt()) + } + } + } + } + + binding.etPrice.setOnFocusChangeListener { _, isFocused -> with(binding.tvPrice) { - if (boolean) { - visibility = View.GONE + visibility = if (isFocused) { + View.GONE } else { - text = decimalFormat.format(viewModel.price.value.toInt()) - visibility = View.VISIBLE + View.VISIBLE } } } @@ -183,18 +195,36 @@ class CarEditFragment : BaseFragment( viewModel.updateState.collectLatest { result -> when (result) { UpdateState.NORMAL -> { - binding.icLoading.root.visibility = View.GONE + binding.btnDone.visibility = View.VISIBLE } UpdateState.LOAD -> { + binding.btnDone.isEnabled = false binding.icLoading.root.visibility = View.VISIBLE } - UpdateState.SUCCESS -> { - // TODO 완료 sanckbar가 too much 인지 생각 - finishUpdateMsg.show() + else -> { + if (result != UpdateState.SUCCESS) { + sendSnackBar(getString(R.string.exception_upload_data)) + } else { + finishUpdateMsg.show() + } findNavController().navigateUp() } - else -> { - Timber.d("차 상세 데이터 update 실패") + } + } + } + } + + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.errorEvent.collectLatest { + if (it is UCMCResult.Error) { + when (it.e) { + DeleteFailException() -> { + sendSnackBar(getString(R.string.exception_delete_image_part)) + } + UpdateFailException() -> { + sendSnackBar(getString(R.string.exception_upload_image_part)) + } } } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/cardetail/edit/CarEditViewModel.kt b/presentation/src/main/java/com/gta/presentation/ui/cardetail/edit/CarEditViewModel.kt index d40f9b0d..b544b8c3 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/cardetail/edit/CarEditViewModel.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/cardetail/edit/CarEditViewModel.kt @@ -10,6 +10,8 @@ import com.gta.domain.model.UCMCResult import com.gta.domain.usecase.cardetail.GetCarDetailDataUseCase import com.gta.domain.usecase.cardetail.edit.UpdateCarDetailDataUseCase import com.gta.domain.usecase.cardetail.edit.UploadCarImagesUseCase +import com.gta.presentation.util.MutableEventFlow +import com.gta.presentation.util.asEventFlow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -54,6 +56,9 @@ class CarEditViewModel @Inject constructor( val updateState: StateFlow get() = _updateState + private val _errorEvent = MutableEventFlow>() + val errorEvent get() = _errorEvent.asEventFlow() + // 초기 이미지 private var initImage: List = emptyList() @@ -100,23 +105,28 @@ class CarEditViewModel @Inject constructor( _updateState.value = UpdateState.LOAD viewModelScope.launch { - _updateState.value = - if ( - updateCarDetailDataUseCase( - carId, - uploadCarImagesUseCase(carId, initImage, images.value).first(), - price.value.toInt(), - comment.value, - if (rentState.value) RentState.AVAILABLE else RentState.UNAVAILABLE, - availableDate.value, - location.value, - coordinate ?: defaultCoordinate - ).first() - ) { - UpdateState.SUCCESS - } else { - UpdateState.FAIL - } + val uploadResult = uploadCarImagesUseCase(carId, initImage, images.value).first() + uploadResult.filterIsInstance().toSet().forEach { + _errorEvent.emit(it) + } + + _updateState.value = if ( + updateCarDetailDataUseCase( + carId, + uploadResult.filter { it is UCMCResult.Success } + .map { (it as UCMCResult.Success).data }, + price.value.toInt(), + comment.value, + if (rentState.value) RentState.AVAILABLE else RentState.UNAVAILABLE, + availableDate.value, + location.value, + coordinate ?: defaultCoordinate + ).first() + ) { + UpdateState.SUCCESS + } else { + UpdateState.FAIL + } } } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/login/LoginActivity.kt b/presentation/src/main/java/com/gta/presentation/ui/login/LoginActivity.kt index 10d231c7..601b247f 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/login/LoginActivity.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/login/LoginActivity.kt @@ -3,6 +3,7 @@ package com.gta.presentation.ui.login import android.Manifest import android.content.Intent import android.content.res.AssetManager +import android.content.res.Resources.NotFoundException import android.os.Build import android.os.Bundle import android.text.method.ScrollingMovementMethod @@ -17,7 +18,8 @@ import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar -import com.gta.domain.model.LoginResult +import com.gta.domain.model.FirestoreException +import com.gta.domain.model.UCMCResult import com.gta.presentation.R import com.gta.presentation.databinding.ActivityLoginBinding import com.gta.presentation.ui.MainActivity @@ -61,7 +63,7 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i } override fun onCreate(savedInstanceState: Bundle?) { - val splashScreen = installSplashScreen() + installSplashScreen() super.onCreate(savedInstanceState) requestNotificationPermission() initCollector() @@ -69,7 +71,7 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i binding.btnLoginGoogle.setOnClickListener { googleLogin() } - setupSplashScreen(splashScreen) + setupSplashScreen() } override fun onResume() { @@ -91,13 +93,14 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i private fun initCollector() { repeatOnStarted(this) { - viewModel.loginEvent.collectLatest { state -> - when (state) { - LoginResult.SUCCESS -> startMainActivity() - LoginResult.NEWUSER -> { - bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + viewModel.loginEvent.collectLatest { result -> + when (result) { + is UCMCResult.Error -> { + handleError(result.e) + } + is UCMCResult.Success -> { + startMainActivity() } - LoginResult.FAILURE -> {} } } } @@ -144,7 +147,7 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i requestActivity.launch(googleSignInClient.signInIntent) } - private fun setupSplashScreen(splashScreen: androidx.core.splashscreen.SplashScreen) { + private fun setupSplashScreen() { val content: View = findViewById(android.R.id.content) content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { @@ -170,4 +173,15 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i } return result } + + private fun handleError(e: Exception) { + when (e) { + is NotFoundException -> { + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + is FirestoreException -> { + Snackbar.make(binding.root, getString(R.string.exception_login), Snackbar.LENGTH_SHORT).show() + } + } + } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/login/LoginViewModel.kt b/presentation/src/main/java/com/gta/presentation/ui/login/LoginViewModel.kt index 4c406eac..f57eb6f7 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/login/LoginViewModel.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/login/LoginViewModel.kt @@ -4,16 +4,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.GoogleAuthProvider -import com.gta.domain.model.LoginResult +import com.gta.domain.model.FirestoreException +import com.gta.domain.model.UCMCResult import com.gta.domain.usecase.login.CheckCurrentUserUseCase import com.gta.domain.usecase.login.SignUpUseCase import com.gta.domain.usecase.user.GetUserProfileUseCase import com.gta.presentation.util.FirebaseUtil +import com.gta.presentation.util.MutableEventFlow +import com.gta.presentation.util.asEventFlow import dagger.hilt.android.lifecycle.HiltViewModel import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.models.User -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber @@ -28,8 +29,8 @@ class LoginViewModel @Inject constructor( private val signUpUseCase: SignUpUseCase ) : ViewModel() { - private val _loginEvent = MutableSharedFlow() - val loginEvent: SharedFlow get() = _loginEvent + private val _loginEvent = MutableEventFlow>() + val loginEvent get() = _loginEvent.asEventFlow() var isLoading = true private set @@ -39,24 +40,19 @@ class LoginViewModel @Inject constructor( val credential = GoogleAuthProvider.getCredential(token, null) auth.signInWithCredential(credential).addOnCompleteListener { task -> if (task.isSuccessful) { - checkCurrentUser(shouldUpdateMessageToken = true) + checkCurrentUser() } else { Timber.e(task.exception) } } } - fun checkCurrentUser(shouldUpdateMessageToken: Boolean = false) { + fun checkCurrentUser() { val user = auth.currentUser if (user != null) { FirebaseUtil.setUid(user) viewModelScope.launch { - handleLoginResult( - checkCurrentUserUseCase( - FirebaseUtil.uid, - shouldUpdateMessageToken - ).first() - ) + handleLoginResult(checkCurrentUserUseCase(FirebaseUtil.uid)) } } else { isLoading = false @@ -65,17 +61,17 @@ class LoginViewModel @Inject constructor( fun signUp() { viewModelScope.launch { - handleLoginResult(signUpUseCase(FirebaseUtil.uid).first()) + handleLoginResult(signUpUseCase(FirebaseUtil.uid)) } } - private fun handleLoginResult(loginResult: LoginResult) { + private fun handleLoginResult(result: UCMCResult) { viewModelScope.launch { - if (loginResult == LoginResult.SUCCESS) { + if (result is UCMCResult.Success) { createChatUser() } else { isLoading = false - _loginEvent.emit(loginResult) + _loginEvent.emit(result) } } } @@ -92,7 +88,11 @@ class LoginViewModel @Inject constructor( user = user, token = chatClient.devToken(user.id) ).enqueue { result -> - val loginResult = if (result.isSuccess) LoginResult.SUCCESS else LoginResult.FAILURE + val loginResult = if (result.isSuccess) { + UCMCResult.Success(Unit) + } else { + UCMCResult.Error(FirestoreException()) + } viewModelScope.launch { _loginEvent.emit(loginResult) } diff --git a/presentation/src/main/java/com/gta/presentation/ui/mypage/mycars/MyCarsListAdapter.kt b/presentation/src/main/java/com/gta/presentation/ui/mypage/mycars/MyCarsListAdapter.kt index c8f76b17..945e16e4 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/mypage/mycars/MyCarsListAdapter.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/mypage/mycars/MyCarsListAdapter.kt @@ -6,17 +6,17 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.gta.domain.model.SimpleCar -import com.gta.presentation.databinding.ItemMypageCarlistBinding +import com.gta.presentation.databinding.ItemOwnerCarBinding class MyCarsListAdapter : ListAdapter(diffUtil) { class CarViewHolder( - private val binding: ItemMypageCarlistBinding, + private val binding: ItemOwnerCarBinding, private val listener: OnItemClickListener? ) : RecyclerView.ViewHolder(binding.root) { fun bind(info: SimpleCar) { - binding.data = info + binding.item = info binding.root.setOnClickListener { if (bindingAdapterPosition != RecyclerView.NO_POSITION) { listener?.onClick(info.id) @@ -39,7 +39,7 @@ class MyCarsListAdapter : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CarViewHolder { val binding = - ItemMypageCarlistBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ItemOwnerCarBinding.inflate(LayoutInflater.from(parent.context), parent, false) return CarViewHolder(binding, listener) } diff --git a/presentation/src/main/java/com/gta/presentation/ui/notification/NotificationListFragment.kt b/presentation/src/main/java/com/gta/presentation/ui/notification/NotificationListFragment.kt index b025e6c0..c423759e 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/notification/NotificationListFragment.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/notification/NotificationListFragment.kt @@ -9,6 +9,8 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import androidx.paging.CombinedLoadStates +import androidx.paging.LoadState import com.gta.domain.model.NotificationType import com.gta.presentation.R import com.gta.presentation.databinding.FragmentNotificationListBinding @@ -22,8 +24,29 @@ class NotificationListFragment : BaseFragment( R.layout.fragment_notification_list ) { - private val viewModel: NotificationViewModel by viewModels() + private val viewModel: NotificationListViewModel by viewModels() private val adapter by lazy { NotificationListAdapter() } + + private val pagingListener: (CombinedLoadStates) -> Unit = { + binding.rvNotification.visibility = View.VISIBLE + binding.pgLoading.visibility = View.GONE + + when (it.source.refresh) { + is LoadState.Loading -> { + binding.pgLoading.visibility = View.VISIBLE + } + is LoadState.NotLoading -> { + if (it.append.endOfPaginationReached && adapter.itemCount < 1) { + binding.rvNotification.visibility = View.GONE + } + } + is LoadState.Error -> { + sendSnackBar(resources.getString(R.string.exception_load_data)) + } + else -> {} + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -34,23 +57,7 @@ class NotificationListFragment : BaseFragment( setItemClickListener(object : NotificationListAdapter.OnItemClickListener { override fun onClick(type: NotificationType, reservation: String) { when (type) { - NotificationType.REQUEST_RESERVATION -> { - findNavController().navigate( - NotificationListFragmentDirections - .actionNotificationListFragmentToReservationCheckFragment( - reservation - ) - ) - } - NotificationType.ACCEPT_RESERVATION -> { - findNavController().navigate( - NotificationListFragmentDirections - .actionNotificationListFragmentToReservationCheckFragment( - reservation - ) - ) - } - NotificationType.DECLINE_RESERVATION -> { + NotificationType.REQUEST_RESERVATION, NotificationType.ACCEPT_RESERVATION, NotificationType.DECLINE_RESERVATION -> { findNavController().navigate( NotificationListFragmentDirections .actionNotificationListFragmentToReservationCheckFragment( @@ -76,6 +83,8 @@ class NotificationListFragment : BaseFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + adapter.addLoadStateListener(pagingListener) + lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.notificationList.collectLatest { @@ -84,4 +93,9 @@ class NotificationListFragment : BaseFragment( } } } + + override fun onPause() { + super.onPause() + adapter.removeLoadStateListener(pagingListener) + } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/notification/NotificationViewModel.kt b/presentation/src/main/java/com/gta/presentation/ui/notification/NotificationListViewModel.kt similarity index 95% rename from presentation/src/main/java/com/gta/presentation/ui/notification/NotificationViewModel.kt rename to presentation/src/main/java/com/gta/presentation/ui/notification/NotificationListViewModel.kt index 7e225631..398bd96d 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/notification/NotificationViewModel.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/notification/NotificationListViewModel.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class NotificationViewModel @Inject constructor( +class NotificationListViewModel @Inject constructor( getNotificationInfo: GetNotificationsInfoUseCase ) : ViewModel() { diff --git a/presentation/src/main/java/com/gta/presentation/ui/reservation/ReservationViewModel.kt b/presentation/src/main/java/com/gta/presentation/ui/reservation/ReservationViewModel.kt index 28e8939c..552443fc 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/reservation/ReservationViewModel.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/reservation/ReservationViewModel.kt @@ -84,7 +84,12 @@ class ReservationViewModel @Inject constructor( } fun createReservation() { - val date = reservationDate.value ?: return + val date = reservationDate.value?.let { + AvailableDate( + it.start, + it.end + 86399999 + ) + } ?: return val price = totalPrice.value ?: return val option = insuranceOption.value ?: return val ownerId = car?.value?.ownerId ?: return diff --git a/presentation/src/main/java/com/gta/presentation/ui/reservation/check/ReservationCheckFragment.kt b/presentation/src/main/java/com/gta/presentation/ui/reservation/check/ReservationCheckFragment.kt index e2110708..c3c43931 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/reservation/check/ReservationCheckFragment.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/reservation/check/ReservationCheckFragment.kt @@ -4,22 +4,24 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.gta.domain.model.CoolDownException +import com.gta.domain.model.FirestoreException import com.gta.domain.model.InsuranceOption import com.gta.domain.model.ReservationState import com.gta.domain.model.UCMCResult import com.gta.presentation.R -import com.gta.presentation.databinding.FragmentReservationRequestBinding +import com.gta.presentation.databinding.FragmentReservationCheckBinding import com.gta.presentation.ui.base.BaseFragment import com.gta.presentation.util.repeatOnStarted import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest @AndroidEntryPoint class ReservationCheckFragment : - BaseFragment(R.layout.fragment_reservation_request) { + BaseFragment(R.layout.fragment_reservation_check) { private val viewModel: ReservationCheckViewModel by viewModels() + private var anchorView: View? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -27,6 +29,51 @@ class ReservationCheckFragment : binding.vm = viewModel viewModel.startCollect() + binding.cvOwner.setOnClickListener { + findNavController().navigate( + ReservationCheckFragmentDirections + .actionReservationCheckFragmentToOwnerProfileFragment( + viewModel.user.value.id + ) + ) + } + + binding.inOwnerProfile.apply { + tvChatting.setOnClickListener { + viewModel.onChattingClick() + } + tvReport.setOnClickListener { + viewModel.onReportClick() + } + } + + repeatOnStarted(viewLifecycleOwner) { + viewModel.navigateChattingEvent.collectLatest { cid -> + findNavController().navigate( + ReservationCheckFragmentDirections.actionReservationCheckFragmentToChattingFragment( + cid + ) + ) + } + } + + repeatOnStarted(viewLifecycleOwner) { + viewModel.reportEvent.collectLatest { result -> + when (result) { + is UCMCResult.Error -> { + handleErrorMessage(result.e) + } + is UCMCResult.Success -> { + sendSnackBar( + message = getString(R.string.report_success), + anchorView = anchorView + + ) + } + } + } + } + repeatOnStarted(viewLifecycleOwner) { viewModel.reservationEvent.collect { result -> when (result) { @@ -38,28 +85,42 @@ class ReservationCheckFragment : else -> null }?.isChecked = true - (if (result.data.state == ReservationState.PENDING.state) View.VISIBLE else View.GONE).also { visibility -> + if (result.data.state == ReservationState.PENDING.state) { + anchorView = binding.btnReservationAccept + View.VISIBLE + } else { + anchorView = null + View.GONE + }.also { visibility -> binding.btnReservationDecline.visibility = visibility binding.btnReservationAccept.visibility = visibility } } is UCMCResult.Error -> { - sendSnackBar(getString(R.string.exception_load_data)) + handleErrorMessage(result.e) } } } } + repeatOnStarted(viewLifecycleOwner) { + viewModel.userEvent.collect { result -> + if (result is UCMCResult.Error) { + handleErrorMessage(result.e) + } + } + } + repeatOnStarted(viewLifecycleOwner) { viewModel.carEvent.collect { result -> if (result is UCMCResult.Error) { - sendSnackBar(getString(R.string.exception_load_data)) + handleErrorMessage(result.e) } } } repeatOnStarted(viewLifecycleOwner) { - viewModel.createReservationEvent.collectLatest { + viewModel.createReservationEvent.collect { if (it) findNavController().popBackStack() } } @@ -69,4 +130,15 @@ class ReservationCheckFragment : viewModel.stopCollect() super.onStop() } + + private fun handleErrorMessage(e: Exception) { + val message = + when (e) { + is FirestoreException -> getString(R.string.report_fail) + is CoolDownException -> getString(R.string.report_cooldown, e.cooldown) + else -> e.message ?: getString(R.string.exception_not_found) + } + + sendSnackBar(message = message, anchorView = anchorView) + } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/reservation/check/ReservationCheckViewModel.kt b/presentation/src/main/java/com/gta/presentation/ui/reservation/check/ReservationCheckViewModel.kt index bc967bd4..eceaa174 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/reservation/check/ReservationCheckViewModel.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/reservation/check/ReservationCheckViewModel.kt @@ -3,29 +3,37 @@ package com.gta.presentation.ui.reservation.check import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.gta.domain.model.CarDetail +import com.gta.domain.model.FirestoreException import com.gta.domain.model.Reservation import com.gta.domain.model.ReservationState +import com.gta.domain.model.SimpleCar import com.gta.domain.model.UCMCResult -import com.gta.domain.usecase.cardetail.GetCarDetailDataUseCase +import com.gta.domain.model.UserProfile +import com.gta.domain.usecase.car.GetSimpleCarUseCase import com.gta.domain.usecase.reservation.FinishReservationUseCase import com.gta.domain.usecase.reservation.GetReservationUseCase +import com.gta.domain.usecase.user.GetUserProfileUseCase +import com.gta.domain.usecase.user.ReportUserUseCase import com.gta.presentation.util.EventFlow +import com.gta.presentation.util.FirebaseUtil import com.gta.presentation.util.MutableEventFlow import com.gta.presentation.util.asEventFlow import dagger.hilt.android.lifecycle.HiltViewModel +import io.getstream.chat.android.client.ChatClient import kotlinx.coroutines.CompletableJob -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.plus +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -33,15 +41,24 @@ class ReservationCheckViewModel @Inject constructor( args: SavedStateHandle, private val finishReservationUseCase: FinishReservationUseCase, private val getReservationUseCase: GetReservationUseCase, - private val getCarDetailDataUseCase: GetCarDetailDataUseCase + private val getSimpleCarUseCase: GetSimpleCarUseCase, + private val getUserProfileUseCase: GetUserProfileUseCase, + private val reportUserUseCase: ReportUserUseCase, + private val chatClient: ChatClient ) : ViewModel() { private val reservationId = args.get("RESERVATION_ID") ?: "정보 없음" - private val _carEvent = MutableEventFlow>() - val carEvent: EventFlow> get() = _carEvent.asEventFlow() + private val _carEvent = MutableEventFlow>() + val carEvent: EventFlow> get() = _carEvent.asEventFlow() - private val _car = MutableStateFlow(CarDetail()) - val car: StateFlow get() = _car + private val _car = MutableStateFlow(SimpleCar()) + val car: StateFlow get() = _car + + private val _userEvent = MutableEventFlow>() + val userEvent: EventFlow> get() = _userEvent.asEventFlow() + + private val _user = MutableStateFlow(UserProfile()) + val user: StateFlow get() = _user private val _reservationEvent = MutableEventFlow>() val reservationEvent: EventFlow> get() = _reservationEvent.asEventFlow() @@ -52,39 +69,64 @@ class ReservationCheckViewModel @Inject constructor( private val _createReservationEvent = MutableEventFlow() val createReservationEvent: EventFlow get() = _createReservationEvent.asEventFlow() + private val _navigateChattingEvent = MutableSharedFlow() + val navigateChattingEvent: SharedFlow get() = _navigateChattingEvent + + private val _reportEvent = MutableEventFlow>() + val reportEvent get() = _reportEvent.asEventFlow() + private lateinit var collectJob: CompletableJob @OptIn(ExperimentalCoroutinesApi::class) fun startCollect() { collectJob = SupervisorJob() - getReservationUseCase(reservationId) - .flowOn(Dispatchers.IO) - .onEach { - if (it is UCMCResult.Success) { - _reservation.emit(it.data) + getReservationUseCase(reservationId).flatMapLatest { target -> + when (target) { + is UCMCResult.Success -> { + _reservation.emit(target.data) + _reservationEvent.emit(target) + if (FirebaseUtil.uid == target.data.ownerId) { + getUserProfileUseCase(target.data.lenderId) + } else { + getUserProfileUseCase(target.data.ownerId) + }.combine(getSimpleCarUseCase(target.data.carId)) { userProfile, simpleCar -> + emitResults(userProfile, simpleCar) + } } - _reservationEvent.emit(it) - }.launchIn(viewModelScope + collectJob) - - reservation.flatMapLatest { target -> - getCarDetailDataUseCase(target.carId) - }.flowOn(Dispatchers.IO) - .onEach { - if (it is UCMCResult.Success) { - _car.emit(it.data) + is UCMCResult.Error -> { + _reservationEvent.emit(target) + flow {} } - _carEvent.emit(it) - }.launchIn(viewModelScope + collectJob) + } + }.launchIn(viewModelScope + collectJob) } fun stopCollect() { collectJob.cancel() } + private fun emitResults(userProfile: UserProfile, simpleCar: SimpleCar) { + viewModelScope.launch { + _user.emit(userProfile) + if (userProfile == UserProfile()) { + _userEvent.emit(UCMCResult.Error(FirestoreException())) + } else { + _userEvent.emit(UCMCResult.Success(userProfile)) + } + + _car.emit(simpleCar) + if (simpleCar == SimpleCar()) { + _carEvent.emit(UCMCResult.Error(FirestoreException())) + } else { + _carEvent.emit(UCMCResult.Success(simpleCar)) + } + } + } + fun finishReservation(accepted: Boolean) { val reservation = reservation.value - val ownerId = car.value.owner.id + val ownerId = reservation.ownerId val state = if (accepted) ReservationState.ACCEPT else ReservationState.CANCEL viewModelScope.launch { @@ -97,4 +139,36 @@ class ReservationCheckViewModel @Inject constructor( ) } } + + fun onChattingClick() { + if (car.value.id == "정보 없음" || user.value.id == "정보 없음") return + val cid = "${reservation.value.lenderId}-${car.value.id}" + createChatChannel(cid) + } + + private fun createChatChannel(cid: String) { + val result = chatClient.createChannel( + channelType = "messaging", + channelId = cid, + memberIds = listOf(reservation.value.ownerId, reservation.value.lenderId), + extraData = emptyMap() + ).execute() + + if (result.isSuccess) { + viewModelScope.launch { + _navigateChattingEvent.emit(result.data().cid) + } + } else { + Timber.tag("chatting").i(result.error().message) + } + } + + fun onReportClick() { + if (user.value.id == "정보 없음" || FirebaseUtil.uid == user.value.id) { + return + } + viewModelScope.launch { + _reportEvent.emit(reportUserUseCase(user.value.id)) + } + } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionFragment.kt b/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionFragment.kt index 09c475cc..e2fe9e55 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionFragment.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionFragment.kt @@ -2,6 +2,7 @@ package com.gta.presentation.ui.transaction import android.os.Bundle import android.view.View +import androidx.navigation.Navigation import androidx.navigation.fragment.navArgs import com.google.android.material.tabs.TabLayoutMediator import com.gta.presentation.R @@ -37,4 +38,10 @@ class TransactionFragment : BaseFragment(R.layout.fr tab.text = tabTitle }.attach() } + + companion object { + fun navigateToReservationCheck(view: View, reservationId: String) { + Navigation.findNavController(view).navigate(TransactionFragmentDirections.actionTransactionFragmentToReservationCheckFragment(reservationId)) + } + } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionListAdapter.kt b/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionListAdapter.kt index 2ad591a8..0f498064 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionListAdapter.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionListAdapter.kt @@ -3,14 +3,14 @@ package com.gta.presentation.ui.transaction import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.gta.domain.model.Transaction import com.gta.presentation.R import com.gta.presentation.databinding.ItemTransactionBinding -class TransactionListAdapter : ListAdapter(TransactionDiffCallback()) { +class TransactionListAdapter : PagingDataAdapter(TransactionDiffCallback()) { class TransactionViewHolder( private val binding: ItemTransactionBinding @@ -20,7 +20,7 @@ class TransactionListAdapter : ListAdapter(R.layout.fragment_transaction_list) { +class TransactionListFragment : + BaseFragment(R.layout.fragment_transaction_list) { private val viewModel: TransactionListViewModel by viewModels() + private val transactionAdapter by lazy { TransactionListAdapter() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.apply { - vm = viewModel - rvTransactionListTransactions.adapter = TransactionListAdapter() + with(binding) { + rvTransactionListTransactions.adapter = transactionAdapter srlTransactionListRefresh.setOnRefreshListener { + transactionAdapter.refresh() srlTransactionListRefresh.isRefreshing = false - viewModel.setTransactions() } } + + initCollector() } - companion object { - fun navigateToTransactionReservationCheck(view: View, reservationId: String) { - Navigation.findNavController(view).navigate(TransactionListFragmentDirections.actionTransactionListFragmentToReservationCheckFragment(reservationId)) + private fun initCollector() { + repeatOnStarted(viewLifecycleOwner) { + transactionAdapter.loadStateFlow.collectLatest { state -> + when (state.append) { + is LoadState.Loading -> { + binding.cpiTransactionListProgress.isVisible = true + } + is LoadState.NotLoading -> { + binding.cpiTransactionListProgress.isVisible = false + binding.tvTransactionListDefaultMsg.isVisible = + state.append.endOfPaginationReached && transactionAdapter.itemCount < 1 + } + is LoadState.Error -> { + sendSnackBar(resources.getString(R.string.exception_load_data)) + } + } + } + } + + repeatOnStarted(viewLifecycleOwner) { + viewModel.transaction.collectLatest { + transactionAdapter.submitData(it) + } } } } diff --git a/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionListViewModel.kt b/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionListViewModel.kt index 2304c176..87a2bcd2 100644 --- a/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionListViewModel.kt +++ b/presentation/src/main/java/com/gta/presentation/ui/transaction/TransactionListViewModel.kt @@ -3,15 +3,16 @@ package com.gta.presentation.ui.transaction import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData import com.gta.domain.model.Transaction import com.gta.domain.usecase.transaction.GetTransactionsUseCase import com.gta.presentation.model.TransactionState import com.gta.presentation.model.TransactionUserState import com.gta.presentation.util.FirebaseUtil import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @HiltViewModel @@ -20,25 +21,21 @@ class TransactionListViewModel @Inject constructor( private val getTransactionsUseCase: GetTransactionsUseCase ) : ViewModel() { - private val userState: TransactionUserState = args.get(TransactionPagerAdapter.USER_STATE_ARG) ?: TransactionUserState.LENDER - private val transactionState: TransactionState = args.get(TransactionPagerAdapter.TRANSACTION_STATE_ARG) ?: TransactionState.TRADING + private val userState: TransactionUserState = + args.get(TransactionPagerAdapter.USER_STATE_ARG) + ?: TransactionUserState.LENDER + private val transactionState: TransactionState = + args.get(TransactionPagerAdapter.TRANSACTION_STATE_ARG) + ?: TransactionState.TRADING - private val _transaction = MutableStateFlow>(emptyList()) - val transaction: StateFlow> get() = _transaction - - init { - setTransactions() - } - - fun setTransactions() { - viewModelScope.launch { - _transaction.emit( - getTransactionsUseCase( - FirebaseUtil.uid, - userState == TransactionUserState.LENDER, - transactionState == TransactionState.TRADING - ) - ) - } - } + val transaction: StateFlow> + get() = getTransactionsUseCase( + FirebaseUtil.uid, + userState == TransactionUserState.LENDER, + transactionState == TransactionState.TRADING + ).stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = PagingData.empty() + ) } diff --git a/presentation/src/main/java/com/gta/presentation/util/DateUtil.kt b/presentation/src/main/java/com/gta/presentation/util/DateUtil.kt index e5065a85..e3d550b9 100644 --- a/presentation/src/main/java/com/gta/presentation/util/DateUtil.kt +++ b/presentation/src/main/java/com/gta/presentation/util/DateUtil.kt @@ -5,7 +5,7 @@ import java.util.* import kotlin.math.abs object DateUtil { - private const val DAY_TIME_UNIT = 60 * 60 * 24 * 1000L + const val DAY_TIME_UNIT = 60 * 60 * 24 * 1000L const val MINUTE_UNIT = 60 * 1000L val dateFormat = SimpleDateFormat("yy/MM/dd", Locale.getDefault()) diff --git a/presentation/src/main/java/com/gta/presentation/util/DateValidator.kt b/presentation/src/main/java/com/gta/presentation/util/DateValidator.kt index a9822078..b983ffb7 100644 --- a/presentation/src/main/java/com/gta/presentation/util/DateValidator.kt +++ b/presentation/src/main/java/com/gta/presentation/util/DateValidator.kt @@ -11,7 +11,7 @@ class DateValidator( ) : CalendarConstraints.DateValidator { override fun isValid(date: Long): Boolean { - return date in reservationRange.first..reservationRange.second && date >= MaterialDatePicker.todayInUtcMilliseconds() && checkInvalidList( + return date in reservationRange.first..reservationRange.second && date > MaterialDatePicker.todayInUtcMilliseconds() && checkInvalidList( date ) } diff --git a/presentation/src/main/res/drawable/bg_bottom_sheet.xml b/presentation/src/main/res/drawable/bg_bottom_sheet.xml new file mode 100644 index 00000000..dbded3af --- /dev/null +++ b/presentation/src/main/res/drawable/bg_bottom_sheet.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/bg_reservation_tag.xml b/presentation/src/main/res/drawable/bg_reservation_tag.xml index 9eedcd07..172a5b9e 100644 --- a/presentation/src/main/res/drawable/bg_reservation_tag.xml +++ b/presentation/src/main/res/drawable/bg_reservation_tag.xml @@ -4,6 +4,6 @@ - + \ No newline at end of file diff --git a/presentation/src/main/res/layout/activity_login.xml b/presentation/src/main/res/layout/activity_login.xml index bfd6be8b..17922cb3 100644 --- a/presentation/src/main/res/layout/activity_login.xml +++ b/presentation/src/main/res/layout/activity_login.xml @@ -42,10 +42,11 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/padding_extra_large" + app:layout_constraintVertical_bias="0.8" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tv_login_logo" /> + app:layout_constraintTop_toTopOf="parent" /> diff --git a/presentation/src/main/res/layout/fragment_car_detail.xml b/presentation/src/main/res/layout/fragment_car_detail.xml index 290e58ee..0e368013 100644 --- a/presentation/src/main/res/layout/fragment_car_detail.xml +++ b/presentation/src/main/res/layout/fragment_car_detail.xml @@ -169,11 +169,12 @@ android:id="@+id/cv_owner" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/padding_small" + android:layout_marginVertical="@dimen/padding_small" app:cardCornerRadius="@dimen/padding_medium" app:layout_constraintEnd_toEndOf="@id/gl_end" app:layout_constraintStart_toStartOf="@id/gl_start" - app:layout_constraintTop_toBottomOf="@+id/tv_owner_label"> + app:layout_constraintTop_toBottomOf="@+id/tv_owner_label" + app:layout_constraintBottom_toBottomOf="parent"> - + android:layout_height="0dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@+id/btn_done"> + android:layout_height="wrap_content"> @@ -96,7 +97,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/padding_small" - android:layout_marginTop="@dimen/padding_medium" + android:layout_marginTop="@dimen/padding_small" android:gravity="end" android:inputType="number" android:maxLength="9" @@ -114,7 +115,6 @@ android:gravity="end|center_vertical" android:textSize="@dimen/font_body_large" android:textStyle="bold" - android:visibility="gone" app:layout_constraintBottom_toBottomOf="@+id/et_price" app:layout_constraintEnd_toEndOf="@+id/et_price" app:layout_constraintStart_toStartOf="@+id/et_price" @@ -135,7 +135,7 @@ android:id="@+id/tv_comment_label" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/padding_medium" + android:layout_marginTop="@dimen/padding_small" android:gravity="start" android:text="@string/car_detail_comment_label" android:textSize="@dimen/font_title_medium" @@ -147,7 +147,7 @@ android:id="@+id/tl_comment_input" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" android:layout_width="0dp" - android:layout_height="0dp" + android:layout_height="wrap_content" app:boxStrokeColor="?attr/colorPrimary" app:boxStrokeErrorColor="@android:color/holo_red_dark" app:counterEnabled="true" @@ -155,7 +155,7 @@ app:errorEnabled="true" app:errorIconDrawable="@null" app:helperText="@string/car_edit_comment_helper_message" - app:layout_constraintBottom_toTopOf="@+id/div_comment_bottom" + android:layout_marginTop="@dimen/padding_small" app:layout_constraintEnd_toEndOf="@id/gl_end" app:layout_constraintHeight_min="230dp" app:layout_constraintStart_toStartOf="@id/gl_start" @@ -182,15 +182,6 @@ app:layout_constraintStart_toStartOf="@id/gl_start" app:layout_constraintTop_toBottomOf="@+id/tl_comment_input" /> - - @@ -293,8 +285,8 @@ - + - + + + + diff --git a/presentation/src/main/res/layout/fragment_my_cars.xml b/presentation/src/main/res/layout/fragment_my_cars.xml index d9569516..e1c59284 100644 --- a/presentation/src/main/res/layout/fragment_my_cars.xml +++ b/presentation/src/main/res/layout/fragment_my_cars.xml @@ -23,7 +23,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/item_mypage_carlist" /> + tools:listitem="@layout/item_owner_car" /> diff --git a/presentation/src/main/res/layout/fragment_notification_list.xml b/presentation/src/main/res/layout/fragment_notification_list.xml index ba435e0a..c887cca6 100644 --- a/presentation/src/main/res/layout/fragment_notification_list.xml +++ b/presentation/src/main/res/layout/fragment_notification_list.xml @@ -1,18 +1,38 @@ + + + android:background="?android:attr/colorBackground" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_notification_list"/> + + diff --git a/presentation/src/main/res/layout/fragment_owner_profile.xml b/presentation/src/main/res/layout/fragment_owner_profile.xml index 0768a128..4cbd8734 100644 --- a/presentation/src/main/res/layout/fragment_owner_profile.xml +++ b/presentation/src/main/res/layout/fragment_owner_profile.xml @@ -34,7 +34,9 @@ android:id="@+id/iv_profile" android:layout_width="100dp" android:layout_height="100dp" - android:layout_marginTop="64dp" + android:layout_marginTop="32dp" + android:layout_marginStart="@dimen/padding_small" + android:scaleType="centerCrop" app:image_uri="@{vm.owner.image}" app:layout_constraintEnd_toEndOf="@+id/tv_nick_name" app:layout_constraintHorizontal_bias="0" @@ -88,19 +90,20 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/padding_small" + android:layout_marginEnd="@dimen/padding_small" android:text="@string/report_it" android:onClick="@{() -> vm.onReportClick()}" app:layout_constraintEnd_toEndOf="@id/gl_end" app:layout_constraintTop_toBottomOf="@+id/iv_profile" /> - diff --git a/presentation/src/main/res/layout/fragment_reservation.xml b/presentation/src/main/res/layout/fragment_reservation.xml index 00197d6a..3b4ebcd3 100644 --- a/presentation/src/main/res/layout/fragment_reservation.xml +++ b/presentation/src/main/res/layout/fragment_reservation.xml @@ -13,270 +13,282 @@ type="com.gta.presentation.ui.reservation.ReservationViewModel" /> - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + + + android:orientation="vertical" + app:layout_constraintGuide_end="395dp" /> - - - + + + + - - - - - - - - - - + + - - + + - - - - - - - - - - - - + android:layout_marginTop="@dimen/padding_medium" + android:textAppearance="@style/Body" + app:date_type="@{DateType.RANGE}" + app:layout_constraintStart_toStartOf="@id/gl_reservation_start" + app:layout_constraintTop_toBottomOf="@id/tv_reservation_title_time" + app:selection="@{vm.reservationDate}" + tools:text="22/11/01 ~ 22/11/02" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_reservation_check.xml b/presentation/src/main/res/layout/fragment_reservation_check.xml new file mode 100644 index 00000000..6e8bf7e5 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_reservation_check.xml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_reservation_request.xml b/presentation/src/main/res/layout/fragment_reservation_request.xml deleted file mode 100644 index b0fe2855..00000000 --- a/presentation/src/main/res/layout/fragment_reservation_request.xml +++ /dev/null @@ -1,242 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_transaction_list.xml b/presentation/src/main/res/layout/fragment_transaction_list.xml index 0bbfc858..128a0ebe 100644 --- a/presentation/src/main/res/layout/fragment_transaction_list.xml +++ b/presentation/src/main/res/layout/fragment_transaction_list.xml @@ -4,15 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" tools:context=".ui.transaction.TransactionListFragment"> - - - - - - - + + diff --git a/presentation/src/main/res/layout/include_owner_profile.xml b/presentation/src/main/res/layout/include_owner_profile.xml index 7d9a3559..324c46ae 100644 --- a/presentation/src/main/res/layout/include_owner_profile.xml +++ b/presentation/src/main/res/layout/include_owner_profile.xml @@ -12,14 +12,17 @@ + android:layout_height="wrap_content" + android:paddingVertical="@dimen/padding_small"> - - + \ No newline at end of file diff --git a/presentation/src/main/res/navigation/nav_main.xml b/presentation/src/main/res/navigation/nav_main.xml index e5b38266..c60565e0 100644 --- a/presentation/src/main/res/navigation/nav_main.xml +++ b/presentation/src/main/res/navigation/nav_main.xml @@ -273,7 +273,7 @@ + android:label="CarEditMapFragment"> @@ -283,12 +283,18 @@ android:id="@+id/reservationCheckFragment" android:name="com.gta.presentation.ui.reservation.check.ReservationCheckFragment" android:label="@string/reservation_request_toolbar" - tools:layout="@layout/fragment_reservation_request"> - + tools:layout="@layout/fragment_reservation_check"> + + + - - - diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index ac2d2f8a..8bdca0fa 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -21,6 +21,7 @@ 정보 없음 내 차 수정하기 + 수정 완료하기 금액 1일 예약 가능 여부 대여 가능 날짜 @@ -58,6 +59,8 @@ %d년식 번호판 : %s + 해당 내용이 없습니다. + 검색어를 입력하세요. 차종 날짜 @@ -169,7 +172,7 @@ 수락하기 알림 목록 예약 요청 확인 - 대여자 정보 + 상대방 정보 ucmc 이 일을 기억할 것입니다. @@ -199,4 +202,8 @@ 데이터를 삭제하는 데 실패했어요. 차 목록을 불러오는 데 실패했어요. 데이터를 불러오는 데 실패했어요. + 일부 사진이 삭제가 실패하였습니다. + 일부 사진이 업로드가 실패하였습니다. + 업로드가 실패하였습니다. + 로그인에 실패했습니다. 다시 시도해주세요. diff --git a/presentation/src/test/java/com/gta/presentation/ExampleUnitTest.kt b/presentation/src/test/java/com/gta/presentation/ExampleUnitTest.kt index c0ade14a..c5563245 100644 --- a/presentation/src/test/java/com/gta/presentation/ExampleUnitTest.kt +++ b/presentation/src/test/java/com/gta/presentation/ExampleUnitTest.kt @@ -1,16 +1 @@ package com.gta.presentation - -import org.junit.Assert.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) - } -}