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)
- }
-}