diff --git a/.github/workflows/backend-cd-prod.yml b/.github/workflows/backend-cd-prod.yml index 52d5b35c8..b749d345f 100644 --- a/.github/workflows/backend-cd-prod.yml +++ b/.github/workflows/backend-cd-prod.yml @@ -129,7 +129,7 @@ jobs: - name: Tag successful deployment as latest if: success() run: | - docker tag ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKERHUB_REPOSITORY:${{ github.sha }-prod ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKERHUB_REPOSITORY:latest + docker tag ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKERHUB_REPOSITORY:${{ github.sha }}-prod ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKERHUB_REPOSITORY:latest docker push ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKERHUB_REPOSITORY:latest - name: Check Docker Process diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 53c658624..804732fef 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -76,7 +76,6 @@ kapt { dependencies { // androidx - implementation(libs.androidx.webkit) implementation(libs.androidx.core.ktx) implementation(libs.androidx.activity) implementation(libs.androidx.hilt.work) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 278c9caa7..23f0dee44 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -96,6 +96,7 @@ ? { - val listType = Types.newParameterizedType(List::class.java, MateEta::class.java) - val adapter: JsonAdapter> = moshi.adapter(listType) - return adapter.fromJson(value) + return value.split(";").map { fromJson(it) } } @TypeConverter fun fromMateEta(type: List): String { - val listType = Types.newParameterizedType(List::class.java, MateEta::class.java) - val adapter: JsonAdapter> = moshi.adapter(listType) - return adapter.toJson(type) + return type.joinToString(";") { toJson(it) } + } + + private fun toJson(mateEta: MateEta): String { + val etaStatusString = + when (mateEta.etaStatus) { + is EtaStatus.Arrived -> """{type:"Arrived"}""" + is EtaStatus.ArrivalSoon -> """{"type":"ArrivalSoon"/"durationMinutes":${mateEta.etaStatus.durationMinutes}}""" + is EtaStatus.LateWarning -> """{"type":"LateWarning"/"durationMinutes":${mateEta.etaStatus.durationMinutes}}""" + is EtaStatus.Late -> """{"type":"Late"/"durationMinutes":${mateEta.etaStatus.durationMinutes}}""" + is EtaStatus.Missing -> """{"type":"Missing"}""" + } + + return """{"mateId":${mateEta.mateId},"nickname":"${mateEta.nickname}","etaStatus":$etaStatusString}""" + } + + private fun fromJson(json: String): MateEta { + val cleanedJson = json.trim().removePrefix("{").removeSuffix("}") + val jsonObject = + cleanedJson.split(",").map { it.trim() } + .associate { + val (key, value) = + it.split(":", limit = 2) + .map { it.trim().removeSurrounding("\"") } + key to value + } + + val mateId = jsonObject["mateId"]!!.toLong() + val nickname = jsonObject["nickname"]!! + val etaStatusJson = jsonObject["etaStatus"]!!.trim() + val etaStatusMap = + etaStatusJson.removePrefix("{").removeSuffix("}").split("/") + .map { it.trim() } + .associate { + val (key, value) = it.split(":", limit = 2).map { it.trim() } + key.removeSurrounding("\"") to value.trim().removeSurrounding("\"") + } + + val etaStatusType = + etaStatusMap["type"] ?: throw IllegalArgumentException("Missing etaStatus type") + val etaStatus = + when (etaStatusType) { + "Arrived" -> EtaStatus.Arrived + "ArrivalSoon" -> { + val durationMinutes = etaStatusMap["durationMinutes"]?.toInt() + EtaStatus.ArrivalSoon(durationMinutes ?: throw IllegalArgumentException("Missing durationMinutes")) + } + + "LateWarning" -> { + val durationMinutes = etaStatusMap["durationMinutes"]?.toInt() + EtaStatus.LateWarning(durationMinutes ?: throw IllegalArgumentException("Missing durationMinutes")) + } + + "Late" -> { + val durationMinutes = etaStatusMap["durationMinutes"]?.toInt() + EtaStatus.Late(durationMinutes ?: throw IllegalArgumentException("Missing durationMinutes")) + } + + "Missing" -> EtaStatus.Missing + else -> throw IllegalArgumentException("Unknown etaStatus type: $etaStatusType") + } + + return MateEta(mateId, nickname, etaStatus) } } diff --git a/android/app/src/main/java/com/mulberry/ody/data/remote/core/entity/meeting/mapper/MatesEtaResponseMapper.kt b/android/app/src/main/java/com/mulberry/ody/data/remote/core/entity/meeting/mapper/MatesEtaResponseMapper.kt index e1e4f5718..855ed2bef 100644 --- a/android/app/src/main/java/com/mulberry/ody/data/remote/core/entity/meeting/mapper/MatesEtaResponseMapper.kt +++ b/android/app/src/main/java/com/mulberry/ody/data/remote/core/entity/meeting/mapper/MatesEtaResponseMapper.kt @@ -2,7 +2,7 @@ package com.mulberry.ody.data.remote.core.entity.meeting.mapper import com.mulberry.ody.data.remote.core.entity.meeting.response.MateEtaResponse import com.mulberry.ody.data.remote.core.entity.meeting.response.MatesEtaResponse -import com.mulberry.ody.domain.model.EtaType +import com.mulberry.ody.domain.model.EtaStatus import com.mulberry.ody.domain.model.MateEta import com.mulberry.ody.domain.model.MateEtaInfo @@ -18,18 +18,17 @@ private fun MateEtaResponse.toMateEta(): MateEta { return MateEta( mateId = mateId, nickname = nickname, - etaType = status.toEtaType(), - durationMinute = durationMinutes.toInt(), + etaStatus = status.toEtaStatus(durationMinutes), ) } -private fun String.toEtaType(): EtaType { +private fun String.toEtaStatus(durationMinutes: Long): EtaStatus { return when (this) { - "LATE_WARNING" -> EtaType.LATE_WARNING - "ARRIVAL_SOON" -> EtaType.ARRIVAL_SOON - "ARRIVED" -> EtaType.ARRIVED - "LATE" -> EtaType.LATE - "MISSING" -> EtaType.MISSING + "ARRIVED" -> EtaStatus.Arrived + "ARRIVAL_SOON" -> EtaStatus.ArrivalSoon(durationMinutes = durationMinutes.toInt()) + "LATE_WARNING" -> EtaStatus.LateWarning(durationMinutes = durationMinutes.toInt()) + "LATE" -> EtaStatus.Late(durationMinutes = durationMinutes.toInt()) + "MISSING" -> EtaStatus.Missing else -> throw IllegalArgumentException("존재하지 않는 Eta Type 입니다.") } } diff --git a/android/app/src/main/java/com/mulberry/ody/di/DBModule.kt b/android/app/src/main/java/com/mulberry/ody/di/DBModule.kt index e5fe07dc6..d84a9c6c1 100644 --- a/android/app/src/main/java/com/mulberry/ody/di/DBModule.kt +++ b/android/app/src/main/java/com/mulberry/ody/di/DBModule.kt @@ -5,8 +5,6 @@ import com.mulberry.ody.data.local.db.EtaReservationDao import com.mulberry.ody.data.local.db.MateEtaInfoDao import com.mulberry.ody.data.local.db.OdyDatabase import com.mulberry.ody.data.local.db.OdyDatastore -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,10 +15,6 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DBModule { - @Provides - @Singleton - fun provideMoshi(): Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() - @Provides @Singleton fun provideDataStore( @@ -33,9 +27,8 @@ object DBModule { @Singleton fun provideOdyDatabase( @ApplicationContext context: Context, - moshi: Moshi, ): OdyDatabase { - return OdyDatabase.create(context, moshi) + return OdyDatabase.create(context) } @Provides diff --git a/android/app/src/main/java/com/mulberry/ody/domain/model/EtaStatus.kt b/android/app/src/main/java/com/mulberry/ody/domain/model/EtaStatus.kt new file mode 100644 index 000000000..3e1c228e3 --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/domain/model/EtaStatus.kt @@ -0,0 +1,13 @@ +package com.mulberry.ody.domain.model + +sealed interface EtaStatus { + data object Arrived : EtaStatus + + data class ArrivalSoon(val durationMinutes: Int) : EtaStatus + + data class LateWarning(val durationMinutes: Int) : EtaStatus + + data class Late(val durationMinutes: Int) : EtaStatus + + data object Missing : EtaStatus +} diff --git a/android/app/src/main/java/com/mulberry/ody/domain/model/EtaType.kt b/android/app/src/main/java/com/mulberry/ody/domain/model/EtaType.kt deleted file mode 100644 index 875b2a270..000000000 --- a/android/app/src/main/java/com/mulberry/ody/domain/model/EtaType.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mulberry.ody.domain.model - -enum class EtaType { - LATE_WARNING, - ARRIVAL_SOON, - ARRIVED, - LATE, - MISSING, -} diff --git a/android/app/src/main/java/com/mulberry/ody/domain/model/MateEta.kt b/android/app/src/main/java/com/mulberry/ody/domain/model/MateEta.kt index 9e82fb29b..dbcc65ecc 100644 --- a/android/app/src/main/java/com/mulberry/ody/domain/model/MateEta.kt +++ b/android/app/src/main/java/com/mulberry/ody/domain/model/MateEta.kt @@ -3,6 +3,5 @@ package com.mulberry.ody.domain.model data class MateEta( val mateId: Long, val nickname: String, - val etaType: EtaType, - val durationMinute: Int, + val etaStatus: EtaStatus, ) diff --git a/android/app/src/main/java/com/mulberry/ody/domain/model/MateEtaInfo.kt b/android/app/src/main/java/com/mulberry/ody/domain/model/MateEtaInfo.kt index bf4583338..a169ad549 100644 --- a/android/app/src/main/java/com/mulberry/ody/domain/model/MateEtaInfo.kt +++ b/android/app/src/main/java/com/mulberry/ody/domain/model/MateEtaInfo.kt @@ -1,8 +1,5 @@ package com.mulberry.ody.domain.model -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) data class MateEtaInfo( val userId: Long, val mateEtas: List, diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/notification/FCMNotification.kt b/android/app/src/main/java/com/mulberry/ody/presentation/notification/FCMNotification.kt index f1d46e0ee..7ef539ab8 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/notification/FCMNotification.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/notification/FCMNotification.kt @@ -6,8 +6,10 @@ import android.app.PendingIntent import android.content.Context import android.media.RingtoneManager import androidx.core.app.NotificationCompat +import androidx.core.app.TaskStackBuilder import com.mulberry.ody.R import com.mulberry.ody.domain.model.NotificationType +import com.mulberry.ody.presentation.meetings.MeetingsActivity import com.mulberry.ody.presentation.room.MeetingRoomActivity import com.mulberry.ody.presentation.room.MeetingRoomActivity.Companion.NAVIGATE_TO_ETA_DASHBOARD import com.mulberry.ody.presentation.room.MeetingRoomActivity.Companion.NAVIGATE_TO_NOTIFICATION_LOG @@ -74,11 +76,15 @@ class FCMNotification NotificationType.DEFAULT -> "" } - val intent = MeetingRoomActivity.getIntent(context, meetingId.toLong(), navigationTarget) - return PendingIntent.getActivity( - context, + val stackBuilder = TaskStackBuilder.create(context) + val parentIntent = MeetingsActivity.getIntent(context) + stackBuilder.addNextIntent(parentIntent) + + val meetingRoomIntent = MeetingRoomActivity.getIntent(context, meetingId.toLong(), navigationTarget) + stackBuilder.addNextIntent(meetingRoomIntent) + + return stackBuilder.getPendingIntent( NOTIFICATION_REQUEST_CODE, - intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) } diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/EtaDashboardBindingAdapter.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/EtaDashboardBindingAdapter.kt index 9d0a9a982..c83e8dc8f 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/EtaDashboardBindingAdapter.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/EtaDashboardBindingAdapter.kt @@ -9,29 +9,18 @@ import androidx.databinding.BindingAdapter import com.mulberry.ody.R import com.mulberry.ody.presentation.room.etadashboard.listener.MissingToolTipListener import com.mulberry.ody.presentation.room.etadashboard.listener.NudgeListener -import com.mulberry.ody.presentation.room.etadashboard.model.EtaDurationMinuteTypeUiModel -import com.mulberry.ody.presentation.room.etadashboard.model.EtaTypeUiModel +import com.mulberry.ody.presentation.room.etadashboard.model.EtaStatusUiModel import com.mulberry.ody.presentation.room.etadashboard.model.MateEtaUiModel @BindingAdapter("etaType") -fun TextView.setBadgeByEtaType(etaTypeUiModel: EtaTypeUiModel) { - text = context.getString(etaTypeUiModel.messageId) - backgroundTintList = ContextCompat.getColorStateList(context, etaTypeUiModel.colorId) +fun TextView.setBadgeByEtaType(etaStatusUiModel: EtaStatusUiModel) { + text = context.getString(etaStatusUiModel.badgeMessageId) + backgroundTintList = ContextCompat.getColorStateList(context, etaStatusUiModel.badgeColorId) } @BindingAdapter("etaStatus") -fun TextView.setEtaStatusText(mateEtaUiModel: MateEtaUiModel) { - text = - when (mateEtaUiModel.getEtaDurationMinuteTypeUiModel()) { - EtaDurationMinuteTypeUiModel.ARRIVED -> context.getString(R.string.status_arrived) - EtaDurationMinuteTypeUiModel.MISSING -> context.getString(R.string.status_missing) - EtaDurationMinuteTypeUiModel.ARRIVAL_SOON -> context.getString(R.string.status_arrival_soon) - EtaDurationMinuteTypeUiModel.ARRIVAL_REMAIN_TIME -> - context.getString( - R.string.status_arrival_remain_time, - mateEtaUiModel.durationMinute, - ) - } +fun TextView.setEtaStatusText(etaStatusUiModel: EtaStatusUiModel) { + text = etaStatusUiModel.etaStatusMessage(context) } @BindingAdapter("isUserSelf", "missingToolTipListener") @@ -46,19 +35,16 @@ fun TextView.setOnClickMissingTooltip( } @BindingAdapter("etaBadgeAnimation") -fun TextView.setEtaBadgeAnimation(etaTypeUiModel: EtaTypeUiModel) { - if ( - etaTypeUiModel == EtaTypeUiModel.LATE || - etaTypeUiModel == EtaTypeUiModel.LATE_WARNING - ) { +fun TextView.setEtaBadgeAnimation(etaStatusUiModel: EtaStatusUiModel) { + if (etaStatusUiModel.canNudge()) { val animation = AnimationUtils.loadAnimation(context, R.anim.bounce_duration_500) this.startAnimation(animation) } } @BindingAdapter("etaBadgeAnimation") -fun TextView.setEtaBadgeAnimation(isPossibleNudge: Boolean) { - if (isPossibleNudge) { +fun TextView.setEtaBadgeAnimation(canNudge: Boolean) { + if (canNudge) { val animation = AnimationUtils.loadAnimation(context, R.anim.bounce_duration_500) this.startAnimation(animation) } @@ -70,10 +56,7 @@ fun TextView.setOnClickNudge( nudgeListener: NudgeListener, ) { setOnClickListener { - if ( - mateEtaUiModel.isUserSelf.not() && - (mateEtaUiModel.etaTypeUiModel == EtaTypeUiModel.LATE || mateEtaUiModel.etaTypeUiModel == EtaTypeUiModel.LATE_WARNING) - ) { + if (mateEtaUiModel.isUserSelf.not() && mateEtaUiModel.etaStatusUiModel.canNudge()) { nudgeListener.nudgeMate( nudgeId = mateEtaUiModel.userId, mateId = mateEtaUiModel.mateId, diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaDurationMinuteTypeUiModel.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaDurationMinuteTypeUiModel.kt deleted file mode 100644 index 6487b47e2..000000000 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaDurationMinuteTypeUiModel.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mulberry.ody.presentation.room.etadashboard.model - -enum class EtaDurationMinuteTypeUiModel { - ARRIVED, - MISSING, - ARRIVAL_SOON, - ARRIVAL_REMAIN_TIME, -} diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaStatusUiModel.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaStatusUiModel.kt new file mode 100644 index 000000000..0691acc89 --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaStatusUiModel.kt @@ -0,0 +1,87 @@ +package com.mulberry.ody.presentation.room.etadashboard.model + +import android.content.Context +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import com.mulberry.ody.R + +sealed class EtaStatusUiModel { + @get:ColorRes + abstract val badgeColorId: Int + + @get:StringRes + abstract val badgeMessageId: Int + + protected fun etaDurationMinutesMessage( + context: Context, + durationMinutes: Int, + ): String { + if (durationMinutes in ARRIVAL_SOON_VALUE_RANGE) { + return context.getString(R.string.status_arrival_soon) + } + return context.getString(R.string.status_arrival_remain_time, durationMinutes) + } + + abstract fun canNudge(): Boolean + + abstract fun etaStatusMessage(context: Context): String + + data object Arrived : EtaStatusUiModel() { + override val badgeColorId: Int = R.color.blue + override val badgeMessageId: Int = R.string.badge_arrived + + override fun etaStatusMessage(context: Context): String { + return context.getString(R.string.status_arrived) + } + + override fun canNudge(): Boolean = false + } + + data class ArrivalSoon(val durationMinutes: Int) : EtaStatusUiModel() { + override val badgeColorId: Int = R.color.green + override val badgeMessageId: Int = R.string.badge_arrival_soon + + override fun etaStatusMessage(context: Context): String { + return super.etaDurationMinutesMessage(context, durationMinutes) + } + + override fun canNudge(): Boolean = false + } + + data class LateWarning(val durationMinutes: Int) : EtaStatusUiModel() { + override val badgeColorId: Int = R.color.yellow + override val badgeMessageId: Int = R.string.badge_late_warning + + override fun etaStatusMessage(context: Context): String { + return super.etaDurationMinutesMessage(context, durationMinutes) + } + + override fun canNudge(): Boolean = true + } + + data class Late(val durationMinutes: Int) : EtaStatusUiModel() { + override val badgeColorId: Int = R.color.red + override val badgeMessageId: Int = R.string.badge_late + + override fun etaStatusMessage(context: Context): String { + return super.etaDurationMinutesMessage(context, durationMinutes) + } + + override fun canNudge(): Boolean = true + } + + data object Missing : EtaStatusUiModel() { + override val badgeColorId: Int = R.color.gray_400 + override val badgeMessageId: Int = R.string.badge_missing + + override fun etaStatusMessage(context: Context): String { + return context.getString(R.string.status_missing) + } + + override fun canNudge(): Boolean = false + } + + companion object { + private val ARRIVAL_SOON_VALUE_RANGE = 1..10 + } +} diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaTypeUiModel.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaTypeUiModel.kt deleted file mode 100644 index 49846821d..000000000 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaTypeUiModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.mulberry.ody.presentation.room.etadashboard.model - -import androidx.annotation.ColorRes -import androidx.annotation.StringRes -import com.mulberry.ody.R - -enum class EtaTypeUiModel( - @ColorRes val colorId: Int, - @StringRes val messageId: Int, -) { - LATE_WARNING(R.color.yellow, R.string.badge_late_warning), - ARRIVAL_SOON(R.color.green, R.string.badge_arrival_soon), - ARRIVED(R.color.blue, R.string.badge_arrived), - LATE(R.color.red, R.string.badge_late), - MISSING(R.color.gray_400, R.string.badge_missing), -} diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaTypeUiModelMapper.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaTypeUiModelMapper.kt index 8c8ac20e1..cce2ae447 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaTypeUiModelMapper.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/EtaTypeUiModelMapper.kt @@ -1,13 +1,13 @@ package com.mulberry.ody.presentation.room.etadashboard.model -import com.mulberry.ody.domain.model.EtaType +import com.mulberry.ody.domain.model.EtaStatus -fun EtaType.toEtaTypeUiModel(): EtaTypeUiModel { +fun EtaStatus.toEtaStatusUiModel(): EtaStatusUiModel { return when (this) { - EtaType.LATE_WARNING -> EtaTypeUiModel.LATE_WARNING - EtaType.ARRIVAL_SOON -> EtaTypeUiModel.ARRIVAL_SOON - EtaType.ARRIVED -> EtaTypeUiModel.ARRIVED - EtaType.LATE -> EtaTypeUiModel.LATE - EtaType.MISSING -> EtaTypeUiModel.MISSING + EtaStatus.Arrived -> EtaStatusUiModel.Arrived + is EtaStatus.ArrivalSoon -> EtaStatusUiModel.ArrivalSoon(durationMinutes) + is EtaStatus.LateWarning -> EtaStatusUiModel.LateWarning(durationMinutes) + is EtaStatus.Late -> EtaStatusUiModel.Late(durationMinutes) + EtaStatus.Missing -> EtaStatusUiModel.Missing } } diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/MateEtaUiModel.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/MateEtaUiModel.kt index 07d6a7313..139c9e538 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/MateEtaUiModel.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/MateEtaUiModel.kt @@ -2,25 +2,9 @@ package com.mulberry.ody.presentation.room.etadashboard.model data class MateEtaUiModel( val nickname: String, - val etaTypeUiModel: EtaTypeUiModel, - val durationMinute: Int, + val etaStatusUiModel: EtaStatusUiModel, val isMissing: Boolean, val isUserSelf: Boolean, val userId: Long, val mateId: Long, -) { - fun getEtaDurationMinuteTypeUiModel(): EtaDurationMinuteTypeUiModel { - return when (durationMinute) { - ARRIVED_VALUE -> EtaDurationMinuteTypeUiModel.ARRIVED - MISSING_VALUE -> EtaDurationMinuteTypeUiModel.MISSING - in ARRIVAL_SOON_VALUE_RANGE -> EtaDurationMinuteTypeUiModel.ARRIVAL_SOON - else -> EtaDurationMinuteTypeUiModel.ARRIVAL_REMAIN_TIME - } - } - - companion object { - const val ARRIVED_VALUE = 0 - const val MISSING_VALUE = -1 - val ARRIVAL_SOON_VALUE_RANGE = 1..10 - } -} +) diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/MateEtaUiModelMapper.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/MateEtaUiModelMapper.kt index 137cccc0b..cda2fb218 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/MateEtaUiModelMapper.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/model/MateEtaUiModelMapper.kt @@ -1,6 +1,6 @@ package com.mulberry.ody.presentation.room.etadashboard.model -import com.mulberry.ody.domain.model.EtaType +import com.mulberry.ody.domain.model.EtaStatus import com.mulberry.ody.domain.model.MateEta import com.mulberry.ody.domain.model.MateEtaInfo @@ -11,9 +11,8 @@ fun MateEtaInfo.toMateEtaUiModels(): List { private fun MateEta.toMateEtaUiModel(userId: Long): MateEtaUiModel { return MateEtaUiModel( nickname = nickname, - etaTypeUiModel = etaType.toEtaTypeUiModel(), - durationMinute = durationMinute, - isMissing = etaType == EtaType.MISSING, + etaStatusUiModel = etaStatus.toEtaStatusUiModel(), + isMissing = etaStatus is EtaStatus.Missing, isUserSelf = userId == mateId, userId = userId, mateId = mateId, diff --git a/android/app/src/main/res/layout-sw1080dp/fragment_meeting_date.xml b/android/app/src/main/res/layout-sw1080dp/fragment_meeting_date.xml index 4241b3e71..4c83549f3 100644 --- a/android/app/src/main/res/layout-sw1080dp/fragment_meeting_date.xml +++ b/android/app/src/main/res/layout-sw1080dp/fragment_meeting_date.xml @@ -15,7 +15,7 @@ android:layout_height="wrap_content" android:layout_marginTop="52dp" android:text="@string/meeting_date_question_front" - android:textColor="@color/purple_800" + android:textColor="@color/secondary" app:layout_constraintEnd_toStartOf="@+id/tv_meet" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" @@ -40,6 +40,7 @@ android:layout_marginHorizontal="@dimen/page_horizontal_margin" android:scaleX="2.0" android:scaleY="2.0" + android:theme="@style/date_picker_style" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/android/app/src/main/res/layout-sw1080dp/fragment_meeting_time.xml b/android/app/src/main/res/layout-sw1080dp/fragment_meeting_time.xml index fcd0cfa13..b7d84bf91 100644 --- a/android/app/src/main/res/layout-sw1080dp/fragment_meeting_time.xml +++ b/android/app/src/main/res/layout-sw1080dp/fragment_meeting_time.xml @@ -20,7 +20,7 @@ android:layout_height="wrap_content" android:layout_marginTop="52dp" android:text="@string/meeting_time_question_front" - android:textColor="@color/purple_800" + android:textColor="@color/secondary" app:layout_constraintEnd_toStartOf="@id/tv_meeting_time_back" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" diff --git a/android/app/src/main/res/layout-sw720dp/fragment_meeting_date.xml b/android/app/src/main/res/layout-sw720dp/fragment_meeting_date.xml index 0e8ccf833..065abad9c 100644 --- a/android/app/src/main/res/layout-sw720dp/fragment_meeting_date.xml +++ b/android/app/src/main/res/layout-sw720dp/fragment_meeting_date.xml @@ -15,7 +15,7 @@ android:layout_height="wrap_content" android:layout_marginTop="52dp" android:text="@string/meeting_date_question_front" - android:textColor="@color/purple_800" + android:textColor="@color/secondary" app:layout_constraintEnd_toStartOf="@+id/tv_meet" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" @@ -40,6 +40,7 @@ android:scaleX="1.5" android:scaleY="1.5" android:layout_marginHorizontal="@dimen/page_horizontal_margin" + android:theme="@style/date_picker_style" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/android/app/src/main/res/layout-sw720dp/fragment_meeting_time.xml b/android/app/src/main/res/layout-sw720dp/fragment_meeting_time.xml index 55726d71a..0503316d3 100644 --- a/android/app/src/main/res/layout-sw720dp/fragment_meeting_time.xml +++ b/android/app/src/main/res/layout-sw720dp/fragment_meeting_time.xml @@ -20,7 +20,7 @@ android:layout_height="wrap_content" android:layout_marginTop="52dp" android:text="@string/meeting_time_question_front" - android:textColor="@color/purple_800" + android:textColor="@color/secondary" app:layout_constraintEnd_toStartOf="@id/tv_meeting_time_back" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" diff --git a/android/app/src/main/res/layout/item_eta_dashboard.xml b/android/app/src/main/res/layout/item_eta_dashboard.xml index 9970282ae..f9377f011 100644 --- a/android/app/src/main/res/layout/item_eta_dashboard.xml +++ b/android/app/src/main/res/layout/item_eta_dashboard.xml @@ -42,19 +42,19 @@ @@ -76,7 +76,7 @@ android:layout_height="wrap_content" android:ellipsize="end" android:lines="1" - app:etaStatus="@{eta}" + app:etaStatus="@{eta.etaStatusUiModel}" tools:text="13분 후 도착" /> - \ No newline at end of file + diff --git a/android/app/src/test/java/com/mulberry/ody/TestFixtures.kt b/android/app/src/test/java/com/mulberry/ody/TestFixtures.kt index 1dd4a91b3..85dc663f2 100644 --- a/android/app/src/test/java/com/mulberry/ody/TestFixtures.kt +++ b/android/app/src/test/java/com/mulberry/ody/TestFixtures.kt @@ -1,7 +1,7 @@ package com.mulberry.ody import com.mulberry.ody.domain.model.Address -import com.mulberry.ody.domain.model.EtaType +import com.mulberry.ody.domain.model.EtaStatus import com.mulberry.ody.domain.model.LogType import com.mulberry.ody.domain.model.Mate import com.mulberry.ody.domain.model.MateEta @@ -28,8 +28,6 @@ val meeting: Meeting = inviteCode, ) -val meetings: List = listOf(meeting) - val meetingCatalog = MeetingCatalog( meetingId, @@ -105,15 +103,14 @@ val notificationLogs: List = ) private val nicknames = listOf("콜리", "올리브", "카키", "해음") -private val mateEtaTypes = - listOf(EtaType.LATE_WARNING, EtaType.ARRIVAL_SOON, EtaType.ARRIVED, EtaType.MISSING) -val mateEtaDurationMinutes = listOf(83, 10, 0, -1) +private val mateEtaStatuses = + listOf(EtaStatus.LateWarning(15), EtaStatus.ArrivalSoon(5), EtaStatus.Arrived, EtaStatus.Missing) val mateEtaInfo = MateEtaInfo( userId = 3L, nicknames.mapIndexed { idx, nickname -> - MateEta(idx.toLong(), nickname, mateEtaTypes[idx], mateEtaDurationMinutes[idx]) + MateEta(idx.toLong(), nickname, mateEtaStatuses[idx]) }, ) diff --git a/android/app/src/test/java/com/mulberry/ody/presentation/room/MeetingRoomViewModelTest.kt b/android/app/src/test/java/com/mulberry/ody/presentation/room/MeetingRoomViewModelTest.kt index 7ba51c717..3bde21977 100644 --- a/android/app/src/test/java/com/mulberry/ody/presentation/room/MeetingRoomViewModelTest.kt +++ b/android/app/src/test/java/com/mulberry/ody/presentation/room/MeetingRoomViewModelTest.kt @@ -7,7 +7,6 @@ import com.mulberry.ody.fake.FakeImageStorage import com.mulberry.ody.fake.FakeMatesEtaRepository import com.mulberry.ody.fake.FakeMeetingRepository import com.mulberry.ody.fake.FakeNotificationLogRepository -import com.mulberry.ody.mateEtaDurationMinutes import com.mulberry.ody.mateEtaInfo import com.mulberry.ody.meeting import com.mulberry.ody.meetingId @@ -61,15 +60,6 @@ class MeetingRoomViewModelTest { } } - @Test - fun `친구들과 나의 남은 시간을 볼 수 있다`() { - runTest { - // then - val durationMinute = viewModel.mateEtaUiModels.first()?.map { it.durationMinute } - assertThat(durationMinute).isEqualTo(mateEtaDurationMinutes) - } - } - @Test fun `약속 id에 맞는 약속을 조회하고 해당하는 로그 목록을 가져온다`() { runTest { diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index cb1c3ee97..962c41196 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -42,12 +42,10 @@ room = "2.6.1" splashScreen = "1.0.1" timber = "5.0.1" viewPager = "1.1.0" -webkit = "1.12.0" workRuntimeKtx = "2.9.1" [libraries] # androidx -androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltWork" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltWork" } diff --git a/backend/src/main/java/com/ody/eta/dto/response/MateEtaResponse.java b/backend/src/main/java/com/ody/eta/dto/response/MateEtaResponse.java index 35829a0c9..e6b573dfa 100644 --- a/backend/src/main/java/com/ody/eta/dto/response/MateEtaResponse.java +++ b/backend/src/main/java/com/ody/eta/dto/response/MateEtaResponse.java @@ -21,18 +21,7 @@ public static MateEtaResponse of(Eta eta, Meeting meeting) { return new MateEtaResponse( eta.getMate().getNickname().getValue(), EtaStatus.of(eta, meeting), - mapMinutes(eta, meeting) + eta.countDownMinutes() ); } - - // 안드측과 협의해서 추후에 View 로직에서 처리 가능한 부분으로 사라질 메서드임 (DTO에 로직이 있는거 걱정 ㄴㄴ) - private static long mapMinutes(Eta eta, Meeting meeting) { - if (eta.isMissing()) { - return -1L; - } - if (eta.isArrivalSoon(meeting)) { - return 1L; - } - return eta.countDownMinutes(); - } } diff --git a/backend/src/main/java/com/ody/mate/controller/MateController.java b/backend/src/main/java/com/ody/mate/controller/MateController.java index ad0b311a6..c2808d80d 100644 --- a/backend/src/main/java/com/ody/mate/controller/MateController.java +++ b/backend/src/main/java/com/ody/mate/controller/MateController.java @@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -39,4 +41,11 @@ public ResponseEntity nudgeMate(@RequestBody @Valid NudgeRequest nudgeRequ mateService.nudge(nudgeRequest); return ResponseEntity.ok().build(); } + + @Override + @DeleteMapping("/meetings/{meetingId}/mate") + public ResponseEntity leave(@AuthMember Member member, @PathVariable Long meetingId) { + mateService.leaveByMeetingIdAndMemberId(meetingId, member.getId()); + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/com/ody/mate/controller/MateControllerSwagger.java b/backend/src/main/java/com/ody/mate/controller/MateControllerSwagger.java index 0db366917..de9701378 100644 --- a/backend/src/main/java/com/ody/mate/controller/MateControllerSwagger.java +++ b/backend/src/main/java/com/ody/mate/controller/MateControllerSwagger.java @@ -51,4 +51,18 @@ ResponseEntity saveV2( @ErrorCode401 @ErrorCode500 ResponseEntity nudgeMate(NudgeRequest nudgeRequest); + + @Operation( + summary = "참여자 약속 방 나가기", + responses = { + @ApiResponse( + responseCode = "204", + description = "참여자 약속 방 나가기 성공" + ) + } + ) + @ErrorCode401 + @ErrorCode404(description = "존재하지 않는 약속이거나 해당 약속 참여자가 아닌 경우") + @ErrorCode500 + ResponseEntity leave(@Parameter(hidden = true) Member member, Long meetingId); } diff --git a/backend/src/main/java/com/ody/mate/repository/MateRepository.java b/backend/src/main/java/com/ody/mate/repository/MateRepository.java index 10bd6f569..77687ffa4 100644 --- a/backend/src/main/java/com/ody/mate/repository/MateRepository.java +++ b/backend/src/main/java/com/ody/mate/repository/MateRepository.java @@ -30,6 +30,7 @@ public interface MateRepository extends JpaRepository { select mate from Mate mate join fetch mate.meeting + join fetch mate.member where mate.meeting.id = :meetingId and mate.member.id = :memberId """) diff --git a/backend/src/main/java/com/ody/mate/service/MateService.java b/backend/src/main/java/com/ody/mate/service/MateService.java index e7326c309..2fe881429 100644 --- a/backend/src/main/java/com/ody/mate/service/MateService.java +++ b/backend/src/main/java/com/ody/mate/service/MateService.java @@ -10,6 +10,7 @@ import com.ody.mate.dto.request.NudgeRequest; import com.ody.mate.dto.response.MateSaveResponseV2; import com.ody.mate.repository.MateRepository; +import com.ody.meeting.domain.Coordinates; import com.ody.meeting.domain.Meeting; import com.ody.meeting.dto.response.MateEtaResponsesV2; import com.ody.member.domain.Member; @@ -54,10 +55,9 @@ public void validateAlreadyAttended(Member member, Meeting meeting) { } private Mate saveMateAndEta(MateSaveRequestV2 mateSaveRequest, Member member, Meeting meeting) { - RouteTime routeTime = routeService.calculateRouteTime( - mateSaveRequest.toOriginCoordinates(), - meeting.getTargetCoordinates() - ); + Coordinates origin = mateSaveRequest.toOriginCoordinates(); + Coordinates target = meeting.getTargetCoordinates(); + RouteTime routeTime = routeService.calculateRouteTime(origin, target); Mate mate = mateRepository.save(mateSaveRequest.toMate(meeting, member, routeTime.getMinutes())); etaService.saveFirstEtaOfMate(mate, routeTime); return mate; @@ -108,13 +108,25 @@ private Mate findByMeetingIdAndMemberId(Long meetingId, Long memberId) { @Transactional public void deleteAllByMember(Member member) { mateRepository.findFetchedAllByMemberId(member.getId()) - .forEach(this::delete); + .forEach(this::withdraw); + } + + @Transactional + public void withdraw(Mate mate) { + notificationService.saveMemberDeletionNotification(mate); + delete(mate); + } + + @Transactional + public void leaveByMeetingIdAndMemberId(Long meetingId, Long memberId) { + Mate mate = findByMeetingIdAndMemberId(meetingId, memberId); + notificationService.saveMateLeaveNotification(mate); + delete(mate); } @Transactional public void delete(Mate mate) { - notificationService.saveMemberDeletionNotification(mate); // TODO: noti 상위 서비스로 묶기 - notificationService.updateAllStatusPendingToDismissedByMateId(mate.getId()); + notificationService.updateAllStatusToDismissByMateIdAndSendAtAfterNow(mate.getId()); notificationService.unSubscribeTopic(mate.getMeeting(), mate.getMember().getDeviceToken()); etaService.deleteByMateId(mate.getId()); mateRepository.deleteById(mate.getId()); diff --git a/backend/src/main/java/com/ody/meeting/controller/MeetingController.java b/backend/src/main/java/com/ody/meeting/controller/MeetingController.java index 6df74d86b..ddb184024 100644 --- a/backend/src/main/java/com/ody/meeting/controller/MeetingController.java +++ b/backend/src/main/java/com/ody/meeting/controller/MeetingController.java @@ -10,11 +10,9 @@ import com.ody.meeting.dto.response.MeetingWithMatesResponse; import com.ody.meeting.service.MeetingService; import com.ody.member.domain.Member; -import com.ody.notification.domain.Notification; import com.ody.notification.dto.response.NotiLogFindResponses; import com.ody.notification.service.NotificationService; import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -68,7 +66,7 @@ public ResponseEntity findAllMeetingLogs( @AuthMember Member member, @PathVariable Long meetingId ) { - NotiLogFindResponses response = notificationService.findAllMeetingLogs(meetingId); + NotiLogFindResponses response = notificationService.findAllNotiLogs(meetingId); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/ody/notification/domain/Notification.java b/backend/src/main/java/com/ody/notification/domain/Notification.java index b524b0f3c..2a2d24164 100644 --- a/backend/src/main/java/com/ody/notification/domain/Notification.java +++ b/backend/src/main/java/com/ody/notification/domain/Notification.java @@ -95,7 +95,17 @@ public static Notification createMemberDeletion(Mate mate) { mate, NotificationType.MEMBER_DELETION, LocalDateTime.now(), - NotificationStatus.DISMISSED, + NotificationStatus.DONE, + null + ); + } + + public static Notification createMateLeave(Mate mate) { + return new Notification( + mate, + NotificationType.LEAVE, + LocalDateTime.now(), + NotificationStatus.DONE, null ); } @@ -111,8 +121,4 @@ public boolean isStatusDismissed() { public void updateStatusToDone() { this.status = NotificationStatus.DONE; } - - public void updateStatusToDismissed() { - this.status = NotificationStatus.DISMISSED; - } } diff --git a/backend/src/main/java/com/ody/notification/domain/NotificationType.java b/backend/src/main/java/com/ody/notification/domain/NotificationType.java index cd42f2fd2..2ac046657 100644 --- a/backend/src/main/java/com/ody/notification/domain/NotificationType.java +++ b/backend/src/main/java/com/ody/notification/domain/NotificationType.java @@ -5,6 +5,7 @@ public enum NotificationType { ENTRY, DEPARTURE_REMINDER, NUDGE, + LEAVE, MEMBER_DELETION, ETA_NOTICE, ; diff --git a/backend/src/main/java/com/ody/notification/repository/NotificationRepository.java b/backend/src/main/java/com/ody/notification/repository/NotificationRepository.java index 8aee04ed6..6e356cc63 100644 --- a/backend/src/main/java/com/ody/notification/repository/NotificationRepository.java +++ b/backend/src/main/java/com/ody/notification/repository/NotificationRepository.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; public interface NotificationRepository extends JpaRepository { @@ -15,10 +16,13 @@ public interface NotificationRepository extends JpaRepository findAllMeetingLogsBeforeThanEqual(Long meetingId, LocalDateTime time); + List findAllByMeetingIdAndSentAtBeforeDateTimeAndStatusIsNotDismissed( + Long meetingId, + LocalDateTime dateTime + ); @Query(""" select noti @@ -39,5 +43,7 @@ public interface NotificationRepository extends JpaRepository findAllMeetingIdAndType(Long meetingId, NotificationType type); - List findAllByMateIdAndStatus(long mateId, NotificationStatus status); + @Modifying(clearAutomatically = true) + @Query("update Notification n set n.status = 'DISMISSED' where n.mate.id = :mateId and n.sendAt > :dateTime") + void updateAllStatusToDismissedByMateIdAndSendAtAfterDateTime(long mateId, LocalDateTime dateTime); } diff --git a/backend/src/main/java/com/ody/notification/service/NotificationService.java b/backend/src/main/java/com/ody/notification/service/NotificationService.java index 25e2c427c..43d74eae2 100644 --- a/backend/src/main/java/com/ody/notification/service/NotificationService.java +++ b/backend/src/main/java/com/ody/notification/service/NotificationService.java @@ -90,8 +90,8 @@ public void schedulePendingNotification() { } @DisabledDeletedFilter - public NotiLogFindResponses findAllMeetingLogs(Long meetingId) { - List notifications = notificationRepository.findAllMeetingLogsBeforeThanEqual( + public NotiLogFindResponses findAllNotiLogs(Long meetingId) { + List notifications = notificationRepository.findAllByMeetingIdAndSentAtBeforeDateTimeAndStatusIsNotDismissed( meetingId, LocalDateTime.now() ); @@ -108,12 +108,8 @@ public void sendNudgeMessage(Mate requestMate, Mate nudgedMate) { } @Transactional - public void updateAllStatusPendingToDismissedByMateId(long mateId) { - List notifications = notificationRepository.findAllByMateIdAndStatus( - mateId, - NotificationStatus.PENDING - ); - notifications.forEach(Notification::updateStatusToDismissed); + public void updateAllStatusToDismissByMateIdAndSendAtAfterNow(long mateId) { + notificationRepository.updateAllStatusToDismissedByMateIdAndSendAtAfterDateTime(mateId, LocalDateTime.now()); } @Transactional @@ -122,6 +118,12 @@ public void saveMemberDeletionNotification(Mate mate) { notificationRepository.save(notification); } + @Transactional + public void saveMateLeaveNotification(Mate mate) { + Notification notification = Notification.createMateLeave(mate); + notificationRepository.save(notification); + } + public void unSubscribeTopic(Meeting meeting, DeviceToken deviceToken) { FcmTopic fcmTopic = new FcmTopic(meeting); fcmSubscriber.unSubscribeTopic(fcmTopic, deviceToken); diff --git a/backend/src/main/java/com/ody/route/domain/RouteTime.java b/backend/src/main/java/com/ody/route/domain/RouteTime.java index 45a5aa20f..0a3a89a6e 100644 --- a/backend/src/main/java/com/ody/route/domain/RouteTime.java +++ b/backend/src/main/java/com/ody/route/domain/RouteTime.java @@ -1,5 +1,6 @@ package com.ody.route.domain; +import java.util.Objects; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -7,7 +8,25 @@ @Getter public class RouteTime { + public static final RouteTime CLOSEST_EXCEPTION_TIME = new RouteTime(-1L); public static final RouteTime ZERO = new RouteTime(0L); private final long minutes; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RouteTime routeTime = (RouteTime) o; + return getMinutes() == routeTime.getMinutes(); + } + + @Override + public int hashCode() { + return Objects.hashCode(getMinutes()); + } } diff --git a/backend/src/main/java/com/ody/route/mapper/OdsayResponseMapper.java b/backend/src/main/java/com/ody/route/mapper/OdsayResponseMapper.java index f5b6a9a11..20a751c04 100644 --- a/backend/src/main/java/com/ody/route/mapper/OdsayResponseMapper.java +++ b/backend/src/main/java/com/ody/route/mapper/OdsayResponseMapper.java @@ -2,6 +2,7 @@ import com.ody.common.exception.OdyBadRequestException; import com.ody.common.exception.OdyServerErrorException; +import com.ody.route.domain.RouteTime; import com.ody.route.dto.OdsayResponse; import java.util.Optional; import lombok.AccessLevel; @@ -15,7 +16,6 @@ public class OdsayResponseMapper { private static final String CLOSE_LOCATION_CODE = "-98"; //출발지-도착지가 700m 이내일 때 private static final String ODSAY_SERVER_ERROR = "500"; private static final String EMPTY_MESSAGE = ""; - private static final long ZERO_TIME = 0L; public static long mapMinutes(OdsayResponse response) { if (response == null) { @@ -23,7 +23,7 @@ public static long mapMinutes(OdsayResponse response) { } if (isCloseLocation(response)) { - return ZERO_TIME; + return RouteTime.CLOSEST_EXCEPTION_TIME.getMinutes(); } if (response.code().isPresent()) { diff --git a/backend/src/main/java/com/ody/route/service/RouteService.java b/backend/src/main/java/com/ody/route/service/RouteService.java index af394d60f..0f55c154f 100644 --- a/backend/src/main/java/com/ody/route/service/RouteService.java +++ b/backend/src/main/java/com/ody/route/service/RouteService.java @@ -13,6 +13,8 @@ @RequiredArgsConstructor public class RouteService { + private static final long CLOSEST_LOCATION_DURATION = 10L; + private final List routeClients; private final ApiCallService apiCallService; @@ -24,7 +26,8 @@ public RouteTime calculateRouteTime(Coordinates origin, Coordinates target) { } try { - RouteTime routeTime = client.calculateRouteTime(origin, target); + + RouteTime routeTime = calculateTime(client, origin, target); apiCallService.increaseCountByClientType(client.getClientType()); log.info("{}를 사용한 소요 시간 계산 성공", client.getClass().getSimpleName()); return routeTime; @@ -36,6 +39,14 @@ public RouteTime calculateRouteTime(Coordinates origin, Coordinates target) { throw new OdyServerErrorException("서버에 장애가 발생했습니다."); } + private RouteTime calculateTime(RouteClient client, Coordinates origin, Coordinates target) { + RouteTime calculatedRouteTime = client.calculateRouteTime(origin, target); + if (calculatedRouteTime.equals(RouteTime.CLOSEST_EXCEPTION_TIME)) { + return new RouteTime(CLOSEST_LOCATION_DURATION); + } + return calculatedRouteTime; + } + private boolean isDisabled(RouteClient client) { return Boolean.FALSE.equals(apiCallService.getEnabledByClientType(client.getClientType())); } diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 3f0769d79..e5f927fae 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -4,9 +4,9 @@ spring: ddl-auto: validate datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ENC(KUJNgDMlvi/imeVHjCO2QekgI4DNxZT9IrW0QlVVkJXJUUQ2DAUOqI91FVH7aFhXVCsfXtf0EUmF/JqN3GpXa/mVj3o4BNbqS3QsU50BgwEEodko+VwHY2ebDHUxlKA1/FXBSmu6p+Rd3rcNEzoBIA==) - username: ENC(jNYhZN6xMCmPQ7GZM+Hu3A==) - password: ENC(VRtr3RjM+XnPvRdLjrPMHyTSzDlEjR7o) + url: ENC(EbLprih9CBRFwayAtzOuQgmLIi1yhY0XKl7h7Ol8ioyJdMtwwHN6caPxplZ7UXUCSCTYfUZ9MgMZ+BLdP2rka97Bx/vmtXo5ysJzdYLLzywI0XTylX4o5N8RQlk1Tx2s7vRI2khoJZ5p6zCdIBaJWw==) + username: ENC(Wd8cwBhFGAJp/RMvZzrOzw==) + password: ENC(X2Gk3hAja3lLSUyJLewcdVWZh3hTJDiJ) flyway: baseline-version: 2 diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 9fcc558e7..0b78f18c1 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -5,17 +5,17 @@ spring: datasource: write: driver-class-name: com.mysql.cj.jdbc.Driver - jdbc-url: jdbc:mysql://database-project.cluster-cqsc6pyqhwww.ap-northeast-2.rds.amazonaws.com:3306/ody - username: ody - password: ENC(2n3gUzzFjknXpHDYwuF4d5BQw6XqLdsX) + jdbc-url: ENC(/pYkPoMxvv/cFWWYEEIJ1/7gtMSjD9WKhM4oSIUEiK3WGRrcXZGELR8s+ozO5hmimO3U99cs3yMKf5B/5uh7RLk4wC3W1IuZ3F0oX3hSEJeq16YkxR1okYZokeE3mZXTxcOHBb3krNg=) + username: ENC(Pjtz7HDMEVm9ScNAjaDEqA==) + password: ENC(9yYOk4n/7co8h0HDfOA20tduvw3htCFa) read: driver-class-name: com.mysql.cj.jdbc.Driver - jdbc-url: jdbc:mysql://database-project.cluster-ro-cqsc6pyqhwww.ap-northeast-2.rds.amazonaws.com:3306/ody - username: ody - password: ENC(2n3gUzzFjknXpHDYwuF4d5BQw6XqLdsX) + jdbc-url: ENC(Ho2WMst/QlNJngqZ1nnRiQIaytH55rjohNVg/5W14vhcd88rAhk16y1yO3zzorKNGSsYV95t/lChWuFA5ivEz4Rh5NaZHjxddhvs1T7HHltl/eC5V1bhPh3+ck98Lk2iil0+RljDy+I=) + username: ENC(Pjtz7HDMEVm9ScNAjaDEqA==) + password: ENC(9yYOk4n/7co8h0HDfOA20tduvw3htCFa) flyway: baseline-version: 2 - + log: file: path: /ody-prod-logs diff --git a/backend/src/main/resources/common.yml b/backend/src/main/resources/common.yml index a6c6e86c7..ecffabcfb 100644 --- a/backend/src/main/resources/common.yml +++ b/backend/src/main/resources/common.yml @@ -22,21 +22,21 @@ springdoc: odsay: url: https://api.odsay.com/v1/api/searchPubTransPathT - api-key: ENC(4+ag02XbL8zS6rWoWHh7WzhKqBj5kaBwBcg04MpLr8nLJefO2PRY8E5o0WpRd3oRtvFv1s9lnWI=) + api-key: ENC(7am2oBS6Y/CCfThV+Yv59ZpjjIu8vOxt8v0/cEvGFDfUemAmSpbz+vfR3bsoDrKoJ/EwmeV6VXo=) google: maps: - api-key: ENC(ISqnJHlmWWslx8mw0KghjXcJsTcxZ97AoylekuQiZN3RGyolV7oejAM3ejRtUllp) + api-key: ENC(B/qgWb19BUQN+qdCHQKP7Ocx4xszftoRM4DfcjSIxCXlwgpfvZW4QpFBMGbNqVsf) auth: - access-key: ENC(jp1TJCM6l22zIzInTUvt9VXrb1WhNm1zY3dFgTSj6ad3585f5K7Q8A==) - access-expiration: ENC(RA/POzGT/Baq/ba9MxmMAQ==) - refresh-key: ENC(5uLKuwtNdyXnr61LRqQJ6Q2sfbhjW5vxgLADB4Na8YgoIkLvcmJk+A==) - refresh-expiration: ENC(GU+tXC1UeXlXFBe9y6IOwnZFtwmRj/a2) + access-key: ENC(Ve3QKOE7PzbTOpQ6b58Oyi1Qrr9DojPgUA+jCwYcmDdSVdP34Z2eKw==) + refresh-key: ENC(Ve3QKOE7PzbTOpQ6b58Oyi1Qrr9DojPgUA+jCwYcmDdSVdP34Z2eKw==) + access-expiration: 7200000 + refresh-expiration: 2419200000 fcm: config: - admin-sdk: ENC(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) + admin-sdk: ENC(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) jasypt: encryptor: @@ -44,7 +44,7 @@ jasypt: kakao: url: https://kapi.kakao.com/v1/user/unlink - admin-key: ENC(b8lXOQquegCbdIgayVYIDBrhkNeyuucBArJScDGtEA2IWFiWeGUViiDhyZg0XpFE) + admin-key: ENC(84d9WaXe+iSolXJ9wIEbJmFEBzeLkgbcP5zkLBRCUJk/Yyic9vwITdVeRArDlFoQ) management: health: @@ -52,7 +52,7 @@ management: enabled: false api-call: - page: ENC(Tmip6pkMmuStDTjkRyspLm2PSd5/ov+T9Gzu2XZJKTjmLsrx4dplPIt/8CarbG2B) + page: ENC(bDtVlLbmI0dmF9v3SFpquBZiPStB9d13/2invvZIxOcs+UdEvexTOpTl+jtHzxjY) allowed-origins: - api-call: ENC(GwAP/KfLoZ6IqqnMPFpqqmg11CXjh1Al6oMlwB+YTekyHGFuRPn1789uAwyJKx2mPX5BRS4I/CoBn6HgnTJwjPL2hm/eHn7VHACtur1NdVc=) + api-call: ENC(QicQ16sDQJlHTuf3E/wIS+VBG0UVfQ62HDBXQbtfM8pmnYsBAH8xmACD7PBVgpLjWAHdJXN6bOKv/Z8BZFFbFyfp5x5f7dNCgok83Cn6jSE=) diff --git a/backend/src/main/resources/db/migration/V7__add_notification_type_leave.sql b/backend/src/main/resources/db/migration/V7__add_notification_type_leave.sql new file mode 100644 index 000000000..4384e98cd --- /dev/null +++ b/backend/src/main/resources/db/migration/V7__add_notification_type_leave.sql @@ -0,0 +1,3 @@ +ALTER TABLE notification DROP CONSTRAINT notification_chk_1; + +ALTER TABLE notification ADD CONSTRAINT notification_chk_1 CHECK (`type` IN ('DEPARTURE_REMINDER', 'ENTRY', 'NUDGE', 'LEAVE', 'MEMBER_DELETION', 'ETA_NOTICE')); diff --git a/backend/src/test/java/com/ody/common/FixtureGenerator.java b/backend/src/test/java/com/ody/common/FixtureGenerator.java index 8dd9f7975..2dbce5bcd 100644 --- a/backend/src/test/java/com/ody/common/FixtureGenerator.java +++ b/backend/src/test/java/com/ody/common/FixtureGenerator.java @@ -165,6 +165,16 @@ public Notification generateNotification(Mate mate, NotificationStatus notificat )); } + public Notification generateNotification(Mate mate, LocalDateTime sendAt, NotificationStatus notificationStatus) { + return notificationRepository.save(new Notification( + mate, + NotificationType.ENTRY, + sendAt, + notificationStatus, + new FcmTopic(mate.getMeeting()) + )); + } + public Notification generateNotification( Mate mate, NotificationType type, diff --git a/backend/src/test/java/com/ody/mate/service/MateServiceTest.java b/backend/src/test/java/com/ody/mate/service/MateServiceTest.java index f5b2980a9..81499cbcc 100644 --- a/backend/src/test/java/com/ody/mate/service/MateServiceTest.java +++ b/backend/src/test/java/com/ody/mate/service/MateServiceTest.java @@ -189,7 +189,7 @@ void unSubscribeTopicWhenDeleteMate() { FcmTopic fcmTopic = new FcmTopic(mate.getMeeting()); DeviceToken deviceToken = mate.getMember().getDeviceToken(); - mateService.delete(mate); + mateService.withdraw(mate); Mockito.verify(fcmSubscriber, Mockito.times(1)).unSubscribeTopic(fcmTopic, deviceToken); } @@ -208,4 +208,17 @@ void unSubscribeAllTopicsWhenDeleteMember() { Mockito.verify(fcmSubscriber, Mockito.times(2)).unSubscribeTopic(any(FcmTopic.class), any(DeviceToken.class)); } + + @DisplayName("약속에 참여하고 있는 mate를 약속 방에서 삭제한다.") + @Test + void deleteMateByMeetingIdAndMemberId() { + Meeting meeting = fixtureGenerator.generateMeeting(); + Member kaki = fixtureGenerator.generateMember("kaki"); + fixtureGenerator.generateMate(meeting, kaki); + + mateService.leaveByMeetingIdAndMemberId(meeting.getId(), kaki.getId()); + + assertThatThrownBy(() -> mateService.findAllByMeetingIdIfMate(kaki, meeting.getId())) + .isInstanceOf(OdyNotFoundException.class); + } } diff --git a/backend/src/test/java/com/ody/notification/repository/NotificationRepositoryTest.java b/backend/src/test/java/com/ody/notification/repository/NotificationRepositoryTest.java index 55a07761a..0a42f4acd 100644 --- a/backend/src/test/java/com/ody/notification/repository/NotificationRepositoryTest.java +++ b/backend/src/test/java/com/ody/notification/repository/NotificationRepositoryTest.java @@ -40,7 +40,7 @@ void findAllMeetingLogsBeforeThanEqual() { NotificationStatus.PENDING ); - List notifications = notificationRepository.findAllMeetingLogsBeforeThanEqual( + List notifications = notificationRepository.findAllByMeetingIdAndSentAtBeforeDateTimeAndStatusIsNotDismissed( odyMeeting.getId(), LocalDateTime.now() ); @@ -80,8 +80,10 @@ void findAllNotificationsById() { notificationRepository.save(pastNotification); notificationRepository.save(futureNotification); - List notifications = notificationRepository.findAllMeetingLogsBeforeThanEqual(odyMeeting.getId(), - LocalDateTime.now()); + List notifications = notificationRepository.findAllByMeetingIdAndSentAtBeforeDateTimeAndStatusIsNotDismissed( + odyMeeting.getId(), + LocalDateTime.now() + ); assertThat(notifications.size()).isOne(); } diff --git a/backend/src/test/java/com/ody/notification/service/NotificationServiceTest.java b/backend/src/test/java/com/ody/notification/service/NotificationServiceTest.java index 5bc575192..a3afa1f00 100644 --- a/backend/src/test/java/com/ody/notification/service/NotificationServiceTest.java +++ b/backend/src/test/java/com/ody/notification/service/NotificationServiceTest.java @@ -111,24 +111,20 @@ void sendSendNudgeMessageMessage() { Mockito.verify(fcmPushSender, Mockito.times(1)).sendNudgeMessage(any(), any()); } - @DisplayName("특정 참여자의 PENDING 상태인 알람을 모두 DISMISSED 상태로 변경한다.") + @DisplayName("특정 참여자의 전송 전 알람을 모두 DISMISSED 상태로 변경한다.") @Test void updateAllStatusPendingToDismissedByMateId() { Mate mate = fixtureGenerator.generateMate(); - fixtureGenerator.generateNotification(mate, NotificationStatus.PENDING); - fixtureGenerator.generateNotification(mate, NotificationStatus.DONE); - fixtureGenerator.generateNotification(mate, NotificationStatus.DISMISSED); - fixtureGenerator.generateNotification(mate, NotificationStatus.PENDING); + LocalDateTime now = LocalDateTime.now(); + fixtureGenerator.generateNotification(mate, now.minusSeconds(1), NotificationStatus.DONE); + fixtureGenerator.generateNotification(mate, now.minusSeconds(1), NotificationStatus.PENDING); + fixtureGenerator.generateNotification(mate, now.plusSeconds(1), NotificationStatus.PENDING); - notificationService.updateAllStatusPendingToDismissedByMateId(mate.getId()); - List actual = notificationRepository.findAll().stream() - .map(Notification::getStatus) - .toList(); + notificationService.updateAllStatusToDismissByMateIdAndSendAtAfterNow(mate.getId()); - assertThat(actual).containsExactly( - NotificationStatus.DISMISSED, + assertThat(notificationRepository.findAll()).extracting(Notification::getStatus).containsExactly( NotificationStatus.DONE, - NotificationStatus.DISMISSED, + NotificationStatus.PENDING, NotificationStatus.DISMISSED ); } @@ -142,7 +138,7 @@ void findAllMeetingLogsOrderOfEntryAndDepartureNotification() { notificationService.saveAndSendNotifications(savedPastMeeting, mate, member.getDeviceToken()); - NotiLogFindResponses allMeetingLogs = notificationService.findAllMeetingLogs(savedPastMeeting.getId()); + NotiLogFindResponses allMeetingLogs = notificationService.findAllNotiLogs(savedPastMeeting.getId()); assertThat(allMeetingLogs.notiLog()).extracting(NotiLogFindResponse::type) .containsExactly(NotificationType.ENTRY.name(), NotificationType.DEPARTURE_REMINDER.name()); @@ -157,10 +153,25 @@ void findAllMeetingLogsIncludingDeletedMember() { fixtureGenerator.generateNotification(deleteMate); fixtureGenerator.generateNotification(mate); - int logCountBeforeDelete = notificationService.findAllMeetingLogs(meeting.getId()).notiLog().size(); + int logCountBeforeDelete = notificationService.findAllNotiLogs(meeting.getId()).notiLog().size(); memberService.delete(deleteMate.getMember()); - int logCountAfterDelete = notificationService.findAllMeetingLogs(meeting.getId()).notiLog().size(); + int logCountAfterDelete = notificationService.findAllNotiLogs(meeting.getId()).notiLog().size(); assertThat(logCountAfterDelete).isEqualTo(logCountBeforeDelete + 1); } + + + @DisplayName("DISMISSED 상태의 알림은 조회되지 않는다.") + @Test + void findAllMeetingLogsExcludingDisMissedStatus() { + Meeting meeting = fixtureGenerator.generateMeeting(); + Mate mate = fixtureGenerator.generateMate(meeting); + fixtureGenerator.generateNotification(mate, NotificationStatus.PENDING); + fixtureGenerator.generateNotification(mate, NotificationStatus.DONE); + fixtureGenerator.generateNotification(mate, NotificationStatus.DISMISSED); + + List notiLogFindResponses = notificationService.findAllNotiLogs(meeting.getId()).notiLog(); + + assertThat(notiLogFindResponses).hasSize(2); + } } diff --git a/backend/src/test/java/com/ody/route/mapper/OdsayResponseMapperTest.java b/backend/src/test/java/com/ody/route/mapper/OdsayResponseMapperTest.java index 7381093ac..d4abb56b5 100644 --- a/backend/src/test/java/com/ody/route/mapper/OdsayResponseMapperTest.java +++ b/backend/src/test/java/com/ody/route/mapper/OdsayResponseMapperTest.java @@ -26,7 +26,7 @@ void mapMinutesSuccess() { .isEqualTo(3L); } - @DisplayName("출, 도착지가 700m 이내일 때 0분을 반환한다") + @DisplayName("출, 도착지가 700m 이내일 때 -1분을 반환한다") @Test void mapZeroMinutesWhenCloseLocation() { OdsayResponse closeLocationResponse = new OdsayResponse( @@ -36,7 +36,7 @@ void mapZeroMinutesWhenCloseLocation() { ); assertThat(OdsayResponseMapper.mapMinutes(closeLocationResponse)) - .isEqualTo(0L); + .isEqualTo(-1L); } @DisplayName("예외 응답 : 500 에러 코드는 OdyServerErrorException을 던진다") diff --git a/backend/src/test/java/com/ody/route/service/OdsayRouteClientTest.java b/backend/src/test/java/com/ody/route/service/OdsayRouteClientTest.java index da1662981..b0b5943d6 100644 --- a/backend/src/test/java/com/ody/route/service/OdsayRouteClientTest.java +++ b/backend/src/test/java/com/ody/route/service/OdsayRouteClientTest.java @@ -39,7 +39,7 @@ void calculateRouteTimeSuccess() throws IOException { mockServer.verify(); } - @DisplayName("도착지와 출발지가 700m 이내일 때, 소요시간 0분을 반환한다") + @DisplayName("도착지와 출발지가 700m 이내일 때, 소요시간 -1분을 반환한다") @Test void calculateRouteTimeWithDistanceWithIn700m() throws IOException { Coordinates origin = new Coordinates("37.505419", "127.050817"); @@ -49,7 +49,7 @@ void calculateRouteTimeWithDistanceWithIn700m() throws IOException { RouteTime routeTime = routeClient.calculateRouteTime(origin, target); - assertThat(routeTime.getMinutes()).isZero(); + assertThat(routeTime.getMinutes()).isEqualTo(-1L); mockServer.verify(); } diff --git a/backend/src/test/java/com/ody/route/service/RouteServiceTest.java b/backend/src/test/java/com/ody/route/service/RouteServiceTest.java index 024ad1bf4..ff4204f19 100644 --- a/backend/src/test/java/com/ody/route/service/RouteServiceTest.java +++ b/backend/src/test/java/com/ody/route/service/RouteServiceTest.java @@ -64,7 +64,30 @@ void calculateRouteTimeByOdsayRouteClient() { () -> verify(apiCallService, times(1)).increaseCountByClientType(odsayRouteClient.getClientType()), () -> verify(apiCallService, Mockito.never()).increaseCountByClientType( googleRouteClient.getClientType()) + ); + } + + @DisplayName("OdsayRouteClient에서 700m 이내라 소요시간 -1을 반환하면 10분으로 소요시간이 전환된다") + @Test + void calculateClosestDurationRouteTimeByOdsayRouteClient() { + Coordinates origin = new Coordinates("37.505419", "127.050817"); + Coordinates target = new Coordinates("37.515253", "127.102895"); + + Mockito.when(odsayRouteClient.calculateRouteTime(origin, target)) + .thenReturn(new RouteTime(-1)); + Mockito.when(googleRouteClient.calculateRouteTime(origin, target)) + .thenReturn(new RouteTime(18)); + + RouteTime result = routeService.calculateRouteTime(origin, target); + RouteTime expectRouteTime = new RouteTime(10); + + assertAll( + () -> assertThat(result).isEqualTo(expectRouteTime), + () -> Mockito.verify(odsayRouteClient, Mockito.times(1)).calculateRouteTime(origin, target), + () -> Mockito.verifyNoInteractions(googleRouteClient), + () -> Mockito.verify(apiCallService, Mockito.times(1)).increaseCountByClientType(odsayRouteClient.getClientType()), + () -> Mockito.verify(apiCallService, Mockito.never()).increaseCountByClientType(googleRouteClient.getClientType()) ); } @@ -85,8 +108,7 @@ void calculateRouteTimeByGoogleRouteClient() { () -> assertThat(result).isEqualTo(expectRouteTime.getMinutes()), () -> verify(odsayRouteClient, times(1)).calculateRouteTime(origin, target), () -> verify(googleRouteClient, times(1)).calculateRouteTime(origin, target), - () -> verify(apiCallService, Mockito.never()).increaseCountByClientType( - odsayRouteClient.getClientType()), + () -> verify(apiCallService, Mockito.never()).increaseCountByClientType(odsayRouteClient.getClientType()), () -> verify(apiCallService, times(1)).increaseCountByClientType(googleRouteClient.getClientType()) ); }