diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 804732fef..6d9246ec0 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -23,8 +23,8 @@ android { applicationId = "com.mulberry.ody" minSdk = 26 targetSdk = 34 - versionCode = 14 - versionName = "1.1.1" + versionCode = 15 + versionName = "1.2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "BASE_DEV_URL", properties["BASE_DEV_URL"].toString()) diff --git a/android/app/src/main/java/com/mulberry/ody/data/local/db/OdyDatabase.kt b/android/app/src/main/java/com/mulberry/ody/data/local/db/OdyDatabase.kt index 6459b61e0..54a90d9ab 100644 --- a/android/app/src/main/java/com/mulberry/ody/data/local/db/OdyDatabase.kt +++ b/android/app/src/main/java/com/mulberry/ody/data/local/db/OdyDatabase.kt @@ -9,7 +9,12 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.mulberry.ody.data.local.entity.eta.MateEtaInfoEntity import com.mulberry.ody.data.local.entity.eta.MateEtaListTypeConverter +import com.mulberry.ody.data.local.entity.eta.OldMateEtaListTypeConverter import com.mulberry.ody.data.local.entity.reserve.EtaReservationEntity +import com.mulberry.ody.domain.model.EtaStatus +import com.mulberry.ody.domain.model.EtaType2 +import com.mulberry.ody.domain.model.MateEta +import com.mulberry.ody.domain.model.MateEta2 @Database( entities = [MateEtaInfoEntity::class, EtaReservationEntity::class], @@ -31,11 +36,57 @@ abstract class OdyDatabase : RoomDatabase() { CREATE TABLE IF NOT EXISTS `eta_reservation` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `meetingId` INTEGER NOT NULL, - `reserveMillis` INTEGER NOT NULL, - `isOpen` INTEGER NOT NULL + `reserveMillis` INTEGER NOT NULL, + `isOpen` INTEGER NOT NULL ) """.trimIndent(), ) + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `eta_info_temp` ( + `meetingId` INTEGER PRIMARY KEY NOT NULL, + `mateId` INTEGER NOT NULL, + `mateEtas` TEXT NOT NULL + ) + """.trimIndent(), + ) + + val cursor = db.query("SELECT * FROM eta_info") + if (cursor.moveToFirst()) { + do { + val meetingId = cursor.getLong(cursor.getColumnIndexOrThrow("meetingId")) + val mateId = cursor.getLong(cursor.getColumnIndexOrThrow("mateId")) + val mateEtasJson = cursor.getString(cursor.getColumnIndexOrThrow("mateEtas")) + + val oldMateEtas: List = OldMateEtaListTypeConverter().fromString(mateEtasJson) ?: emptyList() + val newMateEtas = + oldMateEtas.map { oldMateEta -> + MateEta( + mateId = oldMateEta.mateId, + nickname = oldMateEta.nickname, + etaStatus = + when (oldMateEta.etaType) { + EtaType2.ARRIVED -> EtaStatus.Arrived + EtaType2.ARRIVAL_SOON -> EtaStatus.ArrivalSoon(oldMateEta.durationMinute) + EtaType2.LATE_WARNING -> EtaStatus.LateWarning(oldMateEta.durationMinute) + EtaType2.LATE -> EtaStatus.Late(oldMateEta.durationMinute) + EtaType2.MISSING -> EtaStatus.Missing + }, + ) + } + + val newMateEtasJson = MateEtaListTypeConverter().fromMateEta(newMateEtas) + + db.execSQL( + "INSERT INTO eta_info_temp (meetingId, mateId, mateEtas) VALUES (?, ?, ?)", + arrayOf(meetingId, mateId, newMateEtasJson), + ) + } while (cursor.moveToNext()) + } + + cursor.close() + db.execSQL("DROP TABLE eta_info") + db.execSQL("ALTER TABLE eta_info_temp RENAME TO eta_info") } } diff --git a/android/app/src/main/java/com/mulberry/ody/data/local/entity/eta/MateEtaListTypeConverter.kt b/android/app/src/main/java/com/mulberry/ody/data/local/entity/eta/MateEtaListTypeConverter.kt index 45e39a054..cc4f206b1 100644 --- a/android/app/src/main/java/com/mulberry/ody/data/local/entity/eta/MateEtaListTypeConverter.kt +++ b/android/app/src/main/java/com/mulberry/ody/data/local/entity/eta/MateEtaListTypeConverter.kt @@ -8,7 +8,7 @@ import com.mulberry.ody.domain.model.MateEta @ProvidedTypeConverter class MateEtaListTypeConverter { @TypeConverter - fun fromString(value: String): List? { + fun fromString(value: String): List { return value.split(";").map { fromJson(it) } } @@ -17,7 +17,7 @@ class MateEtaListTypeConverter { return type.joinToString(";") { toJson(it) } } - private fun toJson(mateEta: MateEta): String { + fun toJson(mateEta: MateEta): String { val etaStatusString = when (mateEta.etaStatus) { is EtaStatus.Arrived -> """{type:"Arrived"}""" @@ -38,7 +38,7 @@ class MateEtaListTypeConverter { val (key, value) = it.split(":", limit = 2) .map { it.trim().removeSurrounding("\"") } - key to value + (key to value) } val mateId = jsonObject["mateId"]!!.toLong() diff --git a/android/app/src/main/java/com/mulberry/ody/data/local/entity/eta/OldMateEtaListTypeConverter.kt b/android/app/src/main/java/com/mulberry/ody/data/local/entity/eta/OldMateEtaListTypeConverter.kt new file mode 100644 index 000000000..bd3fea221 --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/data/local/entity/eta/OldMateEtaListTypeConverter.kt @@ -0,0 +1,28 @@ +package com.mulberry.ody.data.local.entity.eta + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +import com.mulberry.ody.domain.model.MateEta2 +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +@ProvidedTypeConverter +class OldMateEtaListTypeConverter( + private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build(), +) { + @TypeConverter + fun fromString(value: String): List? { + val listType = Types.newParameterizedType(List::class.java, MateEta2::class.java) + val adapter: JsonAdapter> = moshi.adapter(listType) + return adapter.fromJson(value) + } + + @TypeConverter + fun fromMateEta(type: List): String { + val listType = Types.newParameterizedType(List::class.java, MateEta2::class.java) + val adapter: JsonAdapter> = moshi.adapter(listType) + return adapter.toJson(type) + } +} diff --git a/android/app/src/main/java/com/mulberry/ody/data/local/repository/DefaultMatesEtaRepository.kt b/android/app/src/main/java/com/mulberry/ody/data/local/repository/DefaultMatesEtaRepository.kt index 864833e73..384d21a0b 100644 --- a/android/app/src/main/java/com/mulberry/ody/data/local/repository/DefaultMatesEtaRepository.kt +++ b/android/app/src/main/java/com/mulberry/ody/data/local/repository/DefaultMatesEtaRepository.kt @@ -64,8 +64,9 @@ class DefaultMatesEtaRepository matesEtaInfoDao.deleteAll() } - override suspend fun deleteEtaReservation(reservationId: Long) { - etaReservationDao.delete(reservationId) + override suspend fun deleteEtaReservation(meetingId: Long) { + etaDashboardAlarm.cancelByMeetingId(meetingId) + etaReservationDao.delete(meetingId) } override suspend fun clearEtaReservation(isReservationPending: Boolean) { @@ -82,7 +83,7 @@ class DefaultMatesEtaRepository entities.forEach { entity -> etaDashboardAlarm.reserve( entity.meetingId, - max(entity.reserveMillis, System.currentTimeMillis()), + max(entity.reserveMillis, System.currentTimeMillis() + ETA_RESERVE_MILLIS_DELAY), entity.isOpen, entity.id, ) @@ -92,6 +93,7 @@ class DefaultMatesEtaRepository private fun MateEtaInfoEntity.toMateEtaInfo(): MateEtaInfo = MateEtaInfo(mateId, mateEtas) companion object { + private const val ETA_RESERVE_MILLIS_DELAY = 3 * 1000 private const val ETA_OPEN_MINUTE = 30L private const val ETA_CLOSE_MINUTE = 2L } diff --git a/android/app/src/main/java/com/mulberry/ody/data/local/service/EtaDashboardAlarm.kt b/android/app/src/main/java/com/mulberry/ody/data/local/service/EtaDashboardAlarm.kt index 4717aa6bc..d3cf7063e 100644 --- a/android/app/src/main/java/com/mulberry/ody/data/local/service/EtaDashboardAlarm.kt +++ b/android/app/src/main/java/com/mulberry/ody/data/local/service/EtaDashboardAlarm.kt @@ -14,7 +14,7 @@ class EtaDashboardAlarm private val context: Context, private val alarmManager: AlarmManager, ) { - private val pendingIntents: MutableList = mutableListOf() + private val pendingIntents: MutableMap = mutableMapOf() fun reserve( meetingId: Long, @@ -22,8 +22,9 @@ class EtaDashboardAlarm isOpen: Boolean, reservationId: Long, ) { + val alarmId = AlarmId(meetingId, reservationId) val pendingIntent = createPendingIntent(meetingId, isOpen, reservationId) - reserveAlarm(reserveMillis, pendingIntent) + reserveAlarm(alarmId, pendingIntent, reserveMillis) } private fun createPendingIntent( @@ -47,10 +48,11 @@ class EtaDashboardAlarm @SuppressLint("ScheduleExactAlarm") private fun reserveAlarm( - triggerAtMillis: Long, + alarmId: AlarmId, pendingIntent: PendingIntent, + triggerAtMillis: Long, ) { - pendingIntents.add(pendingIntent) + pendingIntents[alarmId] = pendingIntent alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, triggerAtMillis, @@ -58,10 +60,18 @@ class EtaDashboardAlarm ) } - fun cancelAll() { - pendingIntents.forEach { - alarmManager.cancel(it) + fun cancelByMeetingId(meetingId: Long) { + val removePendingIntent = pendingIntents.filter { it.key.meetingId == meetingId } + removePendingIntent.forEach { + pendingIntents.remove(it.key) + alarmManager.cancel(it.value) } + } + + fun cancelAll() { + pendingIntents.values.forEach { alarmManager.cancel(it) } pendingIntents.clear() } } + +private data class AlarmId(val meetingId: Long, val reservationId: Long) diff --git a/android/app/src/main/java/com/mulberry/ody/data/remote/core/entity/notification/mapper/NotificationLogsResponseMapper.kt b/android/app/src/main/java/com/mulberry/ody/data/remote/core/entity/notification/mapper/NotificationLogsResponseMapper.kt index baf3b2642..af0b3e2ea 100644 --- a/android/app/src/main/java/com/mulberry/ody/data/remote/core/entity/notification/mapper/NotificationLogsResponseMapper.kt +++ b/android/app/src/main/java/com/mulberry/ody/data/remote/core/entity/notification/mapper/NotificationLogsResponseMapper.kt @@ -1,21 +1,40 @@ package com.mulberry.ody.data.remote.core.entity.notification.mapper import com.mulberry.ody.data.remote.core.entity.notification.response.NotificationLogsResponse -import com.mulberry.ody.domain.model.LogType import com.mulberry.ody.domain.model.NotificationLog +import com.mulberry.ody.domain.model.NotificationLogType +import com.mulberry.ody.domain.model.NotificationLogType.DEFAULT +import com.mulberry.ody.domain.model.NotificationLogType.DEPARTURE +import com.mulberry.ody.domain.model.NotificationLogType.DEPARTURE_REMINDER +import com.mulberry.ody.domain.model.NotificationLogType.ENTRY +import com.mulberry.ody.domain.model.NotificationLogType.MEMBER_DELETION +import com.mulberry.ody.domain.model.NotificationLogType.MEMBER_EXIT +import com.mulberry.ody.domain.model.NotificationLogType.NUDGE import java.time.LocalDateTime import java.time.format.DateTimeFormatter fun NotificationLogsResponse.toNotificationList(): List = this.logList.map { NotificationLog( - type = LogType.from(it.type), + type = it.type.toNotificationLogType(), nickname = it.nickname, createdAt = it.createdAt.parseToLocalDateTime(), imageUrl = it.imageUrl, ) } +private fun String.toNotificationLogType(): NotificationLogType { + return when (this) { + "ENTRY" -> ENTRY + "DEPARTURE_REMINDER" -> DEPARTURE_REMINDER + "DEPARTURE" -> DEPARTURE + "MEMBER_DELETION" -> MEMBER_DELETION + "MEMBER_EXIT" -> MEMBER_EXIT + "NUDGE" -> NUDGE + else -> DEFAULT + } +} + private fun String.parseToLocalDateTime(): LocalDateTime { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") return LocalDateTime.parse(this, formatter) diff --git a/android/app/src/main/java/com/mulberry/ody/data/remote/core/repository/DefaultMeetingRepository.kt b/android/app/src/main/java/com/mulberry/ody/data/remote/core/repository/DefaultMeetingRepository.kt index 2b53e3923..be34cf16b 100644 --- a/android/app/src/main/java/com/mulberry/ody/data/remote/core/repository/DefaultMeetingRepository.kt +++ b/android/app/src/main/java/com/mulberry/ody/data/remote/core/repository/DefaultMeetingRepository.kt @@ -54,11 +54,16 @@ class DefaultMeetingRepository meetingId: Long, mateEtaInfo: MateEtaInfo, ): ApiResult { - val mateEtaInfoEntity = MateEtaInfoEntity(meetingId, mateEtaInfo.userId, mateEtaInfo.mateEtas) + val mateEtaInfoEntity = + MateEtaInfoEntity(meetingId, mateEtaInfo.userId, mateEtaInfo.mateEtas) mateEtaInfoDao.upsert(mateEtaInfoEntity) return ApiResult.Success(Unit) } override suspend fun fetchMeetingCatalogs(): ApiResult> = service.fetchMeetingCatalogs().map { it.toMeetingCatalogs() } + + override suspend fun exitMeeting(meetingId: Long): ApiResult { + return service.exitMeeting(meetingId) + } } diff --git a/android/app/src/main/java/com/mulberry/ody/data/remote/core/service/MeetingService.kt b/android/app/src/main/java/com/mulberry/ody/data/remote/core/service/MeetingService.kt index fb3765fe4..0d657faa4 100644 --- a/android/app/src/main/java/com/mulberry/ody/data/remote/core/service/MeetingService.kt +++ b/android/app/src/main/java/com/mulberry/ody/data/remote/core/service/MeetingService.kt @@ -9,6 +9,7 @@ import com.mulberry.ody.data.remote.core.entity.meeting.response.MeetingCreation import com.mulberry.ody.data.remote.core.entity.meeting.response.MeetingResponse import com.mulberry.ody.domain.apiresult.ApiResult import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.PATCH import retrofit2.http.POST @@ -43,4 +44,9 @@ interface MeetingService { suspend fun postNudge( @Body nudgeRequest: NudgeRequest, ): ApiResult + + @DELETE("/meetings/{meetingId}/mate") + suspend fun exitMeeting( + @Path(value = "meetingId") meetingId: Long, + ): ApiResult } diff --git a/android/app/src/main/java/com/mulberry/ody/domain/model/EtaType2.kt b/android/app/src/main/java/com/mulberry/ody/domain/model/EtaType2.kt new file mode 100644 index 000000000..b6681381f --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/domain/model/EtaType2.kt @@ -0,0 +1,9 @@ +package com.mulberry.ody.domain.model + +enum class EtaType2 { + LATE_WARNING, + ARRIVAL_SOON, + ARRIVED, + LATE, + MISSING, +} diff --git a/android/app/src/main/java/com/mulberry/ody/domain/model/MateEta2.kt b/android/app/src/main/java/com/mulberry/ody/domain/model/MateEta2.kt new file mode 100644 index 000000000..0194d00ca --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/domain/model/MateEta2.kt @@ -0,0 +1,8 @@ +package com.mulberry.ody.domain.model + +data class MateEta2( + val mateId: Long, + val nickname: String, + val etaType: EtaType2, + val durationMinute: Int, +) diff --git a/android/app/src/main/java/com/mulberry/ody/domain/model/NotificationLog.kt b/android/app/src/main/java/com/mulberry/ody/domain/model/NotificationLog.kt index f26a983b0..eb4ac8791 100644 --- a/android/app/src/main/java/com/mulberry/ody/domain/model/NotificationLog.kt +++ b/android/app/src/main/java/com/mulberry/ody/domain/model/NotificationLog.kt @@ -3,7 +3,7 @@ package com.mulberry.ody.domain.model import java.time.LocalDateTime data class NotificationLog( - val type: LogType, + val type: NotificationLogType, val nickname: String, val createdAt: LocalDateTime, val imageUrl: String, diff --git a/android/app/src/main/java/com/mulberry/ody/domain/model/NotificationLogType.kt b/android/app/src/main/java/com/mulberry/ody/domain/model/NotificationLogType.kt new file mode 100644 index 000000000..7bf81fdca --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/domain/model/NotificationLogType.kt @@ -0,0 +1,11 @@ +package com.mulberry.ody.domain.model + +enum class NotificationLogType { + ENTRY, + DEPARTURE_REMINDER, + DEPARTURE, + NUDGE, + MEMBER_DELETION, + MEMBER_EXIT, + DEFAULT, +} diff --git a/android/app/src/main/java/com/mulberry/ody/domain/repository/ody/MatesEtaRepository.kt b/android/app/src/main/java/com/mulberry/ody/domain/repository/ody/MatesEtaRepository.kt index 291b3c8d4..ff9f8c754 100644 --- a/android/app/src/main/java/com/mulberry/ody/domain/repository/ody/MatesEtaRepository.kt +++ b/android/app/src/main/java/com/mulberry/ody/domain/repository/ody/MatesEtaRepository.kt @@ -14,7 +14,7 @@ interface MatesEtaRepository { suspend fun clearEtaFetchingJob() - suspend fun deleteEtaReservation(reservationId: Long) + suspend fun deleteEtaReservation(meetingId: Long) suspend fun clearEtaReservation(isReservationPending: Boolean) diff --git a/android/app/src/main/java/com/mulberry/ody/domain/repository/ody/MeetingRepository.kt b/android/app/src/main/java/com/mulberry/ody/domain/repository/ody/MeetingRepository.kt index 7c01f31e7..b097ba3e7 100644 --- a/android/app/src/main/java/com/mulberry/ody/domain/repository/ody/MeetingRepository.kt +++ b/android/app/src/main/java/com/mulberry/ody/domain/repository/ody/MeetingRepository.kt @@ -29,4 +29,6 @@ interface MeetingRepository { suspend fun fetchMeeting(meetingId: Long): ApiResult suspend fun postNudge(nudge: Nudge): ApiResult + + suspend fun exitMeeting(meetingId: Long): ApiResult } diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/MeetingRoomActivity.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/MeetingRoomActivity.kt index b1c35bc04..367e83edd 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/MeetingRoomActivity.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/MeetingRoomActivity.kt @@ -71,6 +71,11 @@ class MeetingRoomActivity : showSnackBar(R.string.error_guide) } } + lifecycleScope.launch { + viewModel.expiredNudgeTimeLimit.collect { + showSnackBar(R.string.nudge_time_limit_expired) + } + } lifecycleScope.launch { viewModel.isLoading.collect { isLoading -> if (isLoading) { diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/MeetingRoomViewModel.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/MeetingRoomViewModel.kt index 17ace80f1..218e2c885 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/MeetingRoomViewModel.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/MeetingRoomViewModel.kt @@ -10,6 +10,7 @@ import com.mulberry.ody.domain.apiresult.onFailure import com.mulberry.ody.domain.apiresult.onNetworkError import com.mulberry.ody.domain.apiresult.onSuccess import com.mulberry.ody.domain.apiresult.onUnexpected +import com.mulberry.ody.domain.apiresult.suspendOnFailure import com.mulberry.ody.domain.apiresult.suspendOnSuccess import com.mulberry.ody.domain.model.MateEtaInfo import com.mulberry.ody.domain.model.Nudge @@ -47,6 +48,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber +import java.time.Duration import java.time.LocalDateTime class MeetingRoomViewModel @@ -73,7 +75,8 @@ class MeetingRoomViewModel initialValue = null, ) - private val _meeting: MutableStateFlow = MutableStateFlow(MeetingDetailUiModel()) + private val _meeting: MutableStateFlow = + MutableStateFlow(MeetingDetailUiModel()) val meeting: StateFlow = _meeting.asStateFlow() private val _mates: MutableStateFlow> = MutableStateFlow(listOf()) @@ -88,6 +91,17 @@ class MeetingRoomViewModel private val _nudgeSuccessMate: MutableSharedFlow = MutableSharedFlow() val nudgeSuccessMate: SharedFlow get() = _nudgeSuccessMate.asSharedFlow() + private val _expiredNudgeTimeLimit: MutableSharedFlow = MutableSharedFlow() + val expiredNudgeTimeLimit: SharedFlow get() = _expiredNudgeTimeLimit.asSharedFlow() + + private val _nudgeFailMate: MutableSharedFlow = MutableSharedFlow() + val nudgeFailMate: SharedFlow get() = _nudgeFailMate.asSharedFlow() + + private val _exitMeetingRoomEvent: MutableSharedFlow = MutableSharedFlow() + val exitMeetingRoomEvent: SharedFlow get() = _exitMeetingRoomEvent.asSharedFlow() + + private val matesNudgeTimes: MutableMap = mutableMapOf() + init { fetchMeeting() } @@ -97,14 +111,40 @@ class MeetingRoomViewModel mateId: Long, ) { viewModelScope.launch { + val targetMate = mateEtaUiModels.value?.find { it.mateId == mateId } ?: return@launch + handleNudgeAction(nudgeId, mateId, targetMate.nickname) + } + } + + private suspend fun handleNudgeAction( + nudgeId: Long, + mateId: Long, + mateNickname: String, + ) { + val recentNudgeTime = matesNudgeTimes.getOrDefault(mateId, DEFAULT_NUDGE_TIME) + val currentTime = LocalDateTime.now() + + val elapsedSeconds = Duration.between(recentNudgeTime, currentTime).seconds + + if (recentNudgeTime == DEFAULT_NUDGE_TIME || elapsedSeconds >= NUDGE_DELAY_SECONDS) { + matesNudgeTimes[mateId] = currentTime + performNudge(nudgeId, mateId, mateNickname) + } else { + val remainingCooldown = NUDGE_DELAY_SECONDS - elapsedSeconds + _nudgeFailMate.emit(remainingCooldown.toInt()) meetingRepository.postNudge(Nudge(nudgeId, mateId)) .suspendOnSuccess { matesEta.collect { mateEta -> - val mateNickname = mateEta?.mateEtas?.find { it.mateId == mateId }?.nickname ?: return@collect + val mateNickname = + mateEta?.mateEtas?.find { it.mateId == mateId }?.nickname + ?: return@collect _nudgeSuccessMate.emit(mateNickname) } - }.onFailure { code, errorMessage -> - handleError() + }.suspendOnFailure { code, errorMessage -> + when (code) { + 400 -> _expiredNudgeTimeLimit.emit(Unit) + else -> handleError() + } analyticsHelper.logNetworkErrorEvent(TAG, "$code $errorMessage") Timber.e("$code $errorMessage") }.onNetworkError { @@ -114,6 +154,24 @@ class MeetingRoomViewModel } } + private suspend fun performNudge( + nudgeId: Long, + mateId: Long, + mateNickname: String, + ) { + meetingRepository.postNudge(Nudge(nudgeId, mateId)) + .suspendOnSuccess { + _nudgeSuccessMate.emit(mateNickname) + }.onFailure { code, errorMessage -> + handleError() + analyticsHelper.logNetworkErrorEvent(TAG, "$code $errorMessage") + Timber.e("$code $errorMessage") + }.onNetworkError { + handleNetworkError() + lastFailedAction = { nudgeMate(nudgeId, mateId) } + } + } + private fun fetchNotificationLogs() { viewModelScope.launch { startLoading() @@ -230,6 +288,30 @@ class MeetingRoomViewModel } } + fun exitMeetingRoom() { + viewModelScope.launch { + if (_meeting.value.isDefault()) { + handleError() + return@launch + } + + startLoading() + meetingRepository.exitMeeting(_meeting.value.id) + .suspendOnSuccess { + matesEtaRepository.deleteEtaReservation(meetingId) + _exitMeetingRoomEvent.emit(Unit) + }.onFailure { code, errorMessage -> + handleError() + analyticsHelper.logNetworkErrorEvent(TAG, "$code $errorMessage") + Timber.e("$code $errorMessage") + }.onNetworkError { + handleNetworkError() + lastFailedAction = { exitMeetingRoom() } + } + stopLoading() + } + } + @AssistedFactory interface MeetingViewModelFactory { fun create(meetingId: Long): MeetingRoomViewModel @@ -238,6 +320,8 @@ class MeetingRoomViewModel companion object { private const val TAG = "MeetingRoomViewModel" private const val STATE_FLOW_SUBSCRIPTION_TIMEOUT_MILLIS = 5000L + private const val NUDGE_DELAY_SECONDS = 10L + private val DEFAULT_NUDGE_TIME = LocalDateTime.of(2000, 1, 1, 1, 1) fun provideFactory( assistedFactory: MeetingViewModelFactory, diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/EtaDashboardFragment.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/EtaDashboardFragment.kt index 29735ef2a..3ccb749e9 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/EtaDashboardFragment.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/etadashboard/EtaDashboardFragment.kt @@ -27,6 +27,7 @@ import com.mulberry.ody.presentation.room.etadashboard.adapter.MateEtasAdapter import com.mulberry.ody.presentation.room.etadashboard.listener.MissingToolTipListener import com.mulberry.ody.presentation.room.etadashboard.listener.ShareListener import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -94,6 +95,11 @@ class EtaDashboardFragment : showSnackBar(getString(R.string.nudge_success, nickName)) } } + launch { + viewModel.nudgeFailMate.collect { second -> + showSnackBar(getString(R.string.nudge_failure, second)) + } + } } } diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/ExitMeetingRoomDialog.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/ExitMeetingRoomDialog.kt new file mode 100644 index 000000000..91f99e861 --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/ExitMeetingRoomDialog.kt @@ -0,0 +1,81 @@ +package com.mulberry.ody.presentation.room.log + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.mulberry.ody.databinding.DialogExitMeetingRoomBinding +import com.mulberry.ody.presentation.launchWhenStarted +import com.mulberry.ody.presentation.room.MeetingRoomViewModel +import com.mulberry.ody.presentation.room.log.listener.ExitMeetingRoomListener +import kotlinx.coroutines.launch + +class ExitMeetingRoomDialog : DialogFragment(), ExitMeetingRoomListener { + private var _binding: DialogExitMeetingRoomBinding? = null + private val binding get() = _binding!! + + private val viewModel: MeetingRoomViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = DialogExitMeetingRoomBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + initializeView() + initializeBinding() + initializeObserve() + } + + private fun initializeView() { + dialog?.window?.apply { + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + } + + private fun initializeBinding() { + binding.lifecycleOwner = this + binding.exitMeetingRoomListener = this + } + + private fun initializeObserve() { + launchWhenStarted { + launch { + viewModel.meeting.collect { + binding.meetingName = it.name + } + } + launch { + viewModel.exitMeetingRoomEvent.collect { + requireActivity().finish() + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onCancel() { + dismiss() + } + + override fun onExit() { + viewModel.exitMeetingRoom() + } +} diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/NotificationLogBindingAdapter.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/NotificationLogBindingAdapter.kt index 9a2352886..d7ee44c55 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/NotificationLogBindingAdapter.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/NotificationLogBindingAdapter.kt @@ -3,18 +3,19 @@ package com.mulberry.ody.presentation.room.log import android.widget.TextView import androidx.databinding.BindingAdapter import com.mulberry.ody.R -import com.mulberry.ody.domain.model.LogType +import com.mulberry.ody.domain.model.NotificationLogType @BindingAdapter("notificationType") -fun TextView.setTextNotificationType(logType: LogType) { +fun TextView.setTextNotificationType(notificationLogType: NotificationLogType) { val stringRes = - when (logType) { - LogType.ENTRY -> R.string.item_notification_entry - LogType.DEPARTURE_REMINDER -> R.string.item_notification_departure_reminder - LogType.DEPARTURE -> R.string.item_notification_departure - LogType.NUDGE -> R.string.item_notification_nudge - LogType.MEMBER_DELETION -> R.string.item_notification_member_deletion - LogType.DEFAULT -> R.string.item_notification_default + when (notificationLogType) { + NotificationLogType.ENTRY -> R.string.item_notification_entry + NotificationLogType.DEPARTURE_REMINDER -> R.string.item_notification_departure_reminder + NotificationLogType.DEPARTURE -> R.string.item_notification_departure + NotificationLogType.NUDGE -> R.string.item_notification_nudge + NotificationLogType.MEMBER_DELETION -> R.string.item_notification_member_deletion + NotificationLogType.MEMBER_EXIT -> R.string.item_notification_member_exit + NotificationLogType.DEFAULT -> R.string.item_notification_default } text = context.getString(stringRes) } diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/NotificationLogFragment.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/NotificationLogFragment.kt index 268bf5962..bc9e26a74 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/NotificationLogFragment.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/NotificationLogFragment.kt @@ -12,14 +12,14 @@ import com.mulberry.ody.presentation.room.MeetingRoomActivity import com.mulberry.ody.presentation.room.MeetingRoomViewModel import com.mulberry.ody.presentation.room.log.adapter.MatesAdapter import com.mulberry.ody.presentation.room.log.adapter.NotificationLogsAdapter -import com.mulberry.ody.presentation.room.log.listener.InviteCodeCopyListener import com.mulberry.ody.presentation.room.log.listener.MenuListener +import com.mulberry.ody.presentation.room.log.listener.NotificationLogListener import kotlinx.coroutines.launch class NotificationLogFragment : BindingFragment(R.layout.fragment_notification_log), MenuListener, - InviteCodeCopyListener { + NotificationLogListener { private val viewModel: MeetingRoomViewModel by activityViewModels() private val notificationLogsAdapter: NotificationLogsAdapter by lazy { NotificationLogsAdapter() } private val matesAdapter: MatesAdapter by lazy { MatesAdapter() } @@ -37,7 +37,7 @@ class NotificationLogFragment : binding.vm = viewModel binding.backListener = requireActivity() as MeetingRoomActivity binding.menuListener = this - binding.inviteCodeCopyListener = this + binding.notificationLogListener = this binding.rvNotificationLog.adapter = notificationLogsAdapter binding.rvMates.adapter = matesAdapter } @@ -62,7 +62,7 @@ class NotificationLogFragment : } override fun onCopyInviteCode() { - val inviteCode = viewModel.meeting.value?.inviteCode + val inviteCode = viewModel.meeting.value.inviteCode viewModel.shareInviteCode( title = getString(R.string.invite_code_share_title), description = getString(R.string.invite_code_share_description, inviteCode), @@ -71,7 +71,12 @@ class NotificationLogFragment : ) } + override fun onExitMeetingRoom() { + ExitMeetingRoomDialog().show(parentFragmentManager, EXIT_MEETING_ROOM_DIALOG_TAG) + } + companion object { + private const val EXIT_MEETING_ROOM_DIALOG_TAG = "exitMeetingRoomDialog" private const val INVITE_CODE_SHARE_IMAGE_URL = "https://firebasestorage.googleapis.com/" + "v0/b/oddy-4482e.appspot.com/o/odyimage.png?alt=media&token=b3e1db2f-3eb6-46b9-b431-9ac9b6f182a6" diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/listener/ExitMeetingRoomListener.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/listener/ExitMeetingRoomListener.kt new file mode 100644 index 000000000..09341f874 --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/listener/ExitMeetingRoomListener.kt @@ -0,0 +1,7 @@ +package com.mulberry.ody.presentation.room.log.listener + +interface ExitMeetingRoomListener { + fun onCancel() + + fun onExit() +} diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/listener/NotificationLogListener.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/listener/NotificationLogListener.kt new file mode 100644 index 000000000..de72e1bad --- /dev/null +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/listener/NotificationLogListener.kt @@ -0,0 +1,7 @@ +package com.mulberry.ody.presentation.room.log.listener + +interface NotificationLogListener { + fun onCopyInviteCode() + + fun onExitMeetingRoom() +} diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/MeetingDetailUiModel.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/MeetingDetailUiModel.kt index 53553322d..38cd592e1 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/MeetingDetailUiModel.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/MeetingDetailUiModel.kt @@ -1,10 +1,17 @@ package com.mulberry.ody.presentation.room.log.model data class MeetingDetailUiModel( + val id: Long = ID_DEFAULT_VALUE, val name: String = "-", val targetPosition: String = "-", val meetingTime: String = "-", val mates: List = listOf("-"), val inviteCode: String = "-", val isEtaAccessible: Boolean = false, -) +) { + fun isDefault(): Boolean = id == ID_DEFAULT_VALUE + + companion object { + private const val ID_DEFAULT_VALUE = -1L + } +} diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/MeetingDetailUiModelMapper.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/MeetingDetailUiModelMapper.kt index 8c259c528..1f17192b3 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/MeetingDetailUiModelMapper.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/MeetingDetailUiModelMapper.kt @@ -10,6 +10,7 @@ fun Meeting.toMeetingUiModel(): MeetingDetailUiModel { val meetingDateTime = LocalDateTime.of(meetingDate, meetingTime) return MeetingDetailUiModel( + id, name, targetPosition, toMeetingDateTime(meetingDate, meetingTime), diff --git a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/NotificationLogUiModel.kt b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/NotificationLogUiModel.kt index ed52cfada..c72731d29 100644 --- a/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/NotificationLogUiModel.kt +++ b/android/app/src/main/java/com/mulberry/ody/presentation/room/log/model/NotificationLogUiModel.kt @@ -1,9 +1,9 @@ package com.mulberry.ody.presentation.room.log.model -import com.mulberry.ody.domain.model.LogType +import com.mulberry.ody.domain.model.NotificationLogType data class NotificationLogUiModel( - val type: LogType, + val type: NotificationLogType, val nickname: String, val created: String, val imageUrl: String, diff --git a/android/app/src/main/res/drawable/ic_exit.xml b/android/app/src/main/res/drawable/ic_exit.xml new file mode 100644 index 000000000..616280fd4 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_exit.xml @@ -0,0 +1,20 @@ + + + + diff --git a/android/app/src/main/res/layout/activity_meetings.xml b/android/app/src/main/res/layout/activity_meetings.xml index 1fc7be339..2ab9999e8 100644 --- a/android/app/src/main/res/layout/activity_meetings.xml +++ b/android/app/src/main/res/layout/activity_meetings.xml @@ -84,12 +84,13 @@ android:backgroundTint="@color/primaryVariant" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/fab_meetings_navigator" - app:layout_constraintEnd_toEndOf="@id/fab_meetings_navigator"> + app:layout_constraintEnd_toEndOf="@id/fab_meetings_navigator" + tools:visibility="visible"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/dialog_withdrawal.xml b/android/app/src/main/res/layout/dialog_withdrawal.xml index d6089023a..e644ea2d0 100644 --- a/android/app/src/main/res/layout/dialog_withdrawal.xml +++ b/android/app/src/main/res/layout/dialog_withdrawal.xml @@ -98,8 +98,8 @@ style="@style/pretendard_medium_18" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginVertical="22dp" android:gravity="center" + android:paddingVertical="22dp" android:text="@string/cancel_button" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/guide_line_2" @@ -123,6 +123,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:gravity="center" + android:paddingVertical="22dp" android:text="@string/setting_withdrawal_button" android:textColor="@color/red" app:layout_constraintBottom_toBottomOf="parent" diff --git a/android/app/src/main/res/layout/fragment_notification_log.xml b/android/app/src/main/res/layout/fragment_notification_log.xml index 8145fb836..73dcc05b3 100644 --- a/android/app/src/main/res/layout/fragment_notification_log.xml +++ b/android/app/src/main/res/layout/fragment_notification_log.xml @@ -18,8 +18,8 @@ type="com.mulberry.ody.presentation.room.log.listener.MenuListener" /> + name="notificationLogListener" + type="com.mulberry.ody.presentation.room.log.listener.NotificationLogListener" /> + android:layout_gravity="end" + android:background="@color/septenary"> + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 8062a7918..1487561a2 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -13,6 +13,7 @@ (이)가 출발했어요. "(이)가 재촉 받았어요. \uD83D\uDC40" (이)가 오디를 떠났어요. + (이)가 나갔어요. @@ -136,6 +137,8 @@ 위치 권한을 켜서 오디인지 공유해 보세요. 친구의 위치 권한이 꺼져있어요. %s에게 빨리 오라고 재촉했어요! + 약속 시간 30분 이후에는 재촉할 수 없어요 + %d초 후에 다시 재촉할 수 있어요. 친구들의 도착 예정 정보를 확인해보세요! 확인하러 가기 친구와 도착 정보를 공유하고 있어요. @@ -154,6 +157,10 @@ 주소나 상호명을 검색해 보세요 검색 결과가\n존재하지 않아요. + + 약속을 정말 나가실 건가요? + 나가기 + 다음 확인 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 85dc663f2..1b32c1b26 100644 --- a/android/app/src/test/java/com/mulberry/ody/TestFixtures.kt +++ b/android/app/src/test/java/com/mulberry/ody/TestFixtures.kt @@ -2,13 +2,13 @@ package com.mulberry.ody import com.mulberry.ody.domain.model.Address 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 import com.mulberry.ody.domain.model.MateEtaInfo import com.mulberry.ody.domain.model.Meeting import com.mulberry.ody.domain.model.MeetingCatalog import com.mulberry.ody.domain.model.NotificationLog +import com.mulberry.ody.domain.model.NotificationLogType import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -28,6 +28,8 @@ val meeting: Meeting = inviteCode, ) +val meetings: List = listOf(meeting) + val meetingCatalog = MeetingCatalog( meetingId, @@ -47,55 +49,55 @@ val meetingCatalogs: List = val notificationLogs: List = listOf( NotificationLog( - LogType.ENTRY, + NotificationLogType.ENTRY, "A", LocalDateTime.of(2024, 7, 7, 14, 30), "", ), NotificationLog( - LogType.ENTRY, + NotificationLogType.ENTRY, "B", LocalDateTime.of(2024, 7, 7, 14, 31), "", ), NotificationLog( - LogType.ENTRY, + NotificationLogType.ENTRY, "C", LocalDateTime.of(2024, 7, 7, 14, 32), "", ), NotificationLog( - LogType.DEPARTURE_REMINDER, + NotificationLogType.DEPARTURE_REMINDER, "A", LocalDateTime.of(2024, 7, 7, 14, 33), "", ), NotificationLog( - LogType.DEPARTURE_REMINDER, + NotificationLogType.DEPARTURE_REMINDER, "B", LocalDateTime.of(2024, 7, 7, 14, 34), "", ), NotificationLog( - LogType.DEPARTURE_REMINDER, + NotificationLogType.DEPARTURE_REMINDER, "C", LocalDateTime.of(2024, 7, 7, 14, 35), "", ), NotificationLog( - LogType.DEPARTURE, + NotificationLogType.DEPARTURE, "A", LocalDateTime.of(2024, 7, 7, 14, 36), "", ), NotificationLog( - LogType.DEPARTURE, + NotificationLogType.DEPARTURE, "B", LocalDateTime.of(2024, 7, 7, 14, 37), "", ), NotificationLog( - LogType.DEPARTURE, + NotificationLogType.DEPARTURE, "C", LocalDateTime.of(2024, 7, 7, 14, 38), "", diff --git a/android/app/src/test/java/com/mulberry/ody/fake/FakeMatesEtaRepository.kt b/android/app/src/test/java/com/mulberry/ody/fake/FakeMatesEtaRepository.kt index 1e89be208..818b6ae6d 100644 --- a/android/app/src/test/java/com/mulberry/ody/fake/FakeMatesEtaRepository.kt +++ b/android/app/src/test/java/com/mulberry/ody/fake/FakeMatesEtaRepository.kt @@ -19,7 +19,7 @@ object FakeMatesEtaRepository : MatesEtaRepository { override suspend fun clearEtaFetchingJob() = Unit - override suspend fun deleteEtaReservation(reservationId: Long) = Unit + override suspend fun deleteEtaReservation(meetingId: Long) = Unit override suspend fun clearEtaReservation(isReservationPending: Boolean) = Unit diff --git a/android/app/src/test/java/com/mulberry/ody/fake/FakeMeetingRepository.kt b/android/app/src/test/java/com/mulberry/ody/fake/FakeMeetingRepository.kt index a8ba29675..a266804ab 100644 --- a/android/app/src/test/java/com/mulberry/ody/fake/FakeMeetingRepository.kt +++ b/android/app/src/test/java/com/mulberry/ody/fake/FakeMeetingRepository.kt @@ -47,4 +47,6 @@ object FakeMeetingRepository : MeetingRepository { override suspend fun fetchMeeting(meetingId: Long): ApiResult = ApiResult.Success(meeting) override suspend fun postNudge(nudge: Nudge): ApiResult = ApiResult.Success(Unit) + + override suspend fun exitMeeting(meetingId: Long): ApiResult = ApiResult.Success(Unit) } 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 3bde21977..34849ba78 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 @@ -76,10 +76,8 @@ class MeetingRoomViewModelTest { fun `친구 재촉을 하면 친구 재촉이 성공한다`() { runTest { // when - val actual = - viewModel.nudgeSuccessMate.valueOnAction { - viewModel.nudgeMate(1, 0) - } + viewModel.mateEtaUiModels.first() + val actual = viewModel.nudgeSuccessMate.valueOnAction { viewModel.nudgeMate(1, 0) } // then assertThat(actual).isEqualTo("콜리") diff --git a/backend/src/main/java/com/ody/eta/service/EtaService.java b/backend/src/main/java/com/ody/eta/service/EtaService.java index 98432f26e..3f98993db 100644 --- a/backend/src/main/java/com/ody/eta/service/EtaService.java +++ b/backend/src/main/java/com/ody/eta/service/EtaService.java @@ -15,6 +15,7 @@ import com.ody.util.DistanceCalculator; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.slf4j.MDC; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -60,6 +61,7 @@ private void updateMateEta(MateEtaRequest mateEtaRequest, Eta mateEta, Meeting m } if (isRouteClientCallTime(mateEta)) { + MDC.put("mateId", mateEta.getMate().getId().toString()); RouteTime routeTime = routeService.calculateRouteTime( mateEtaRequest.toCoordinates(), meeting.getTargetCoordinates() 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 2fe881429..fd6658d9b 100644 --- a/backend/src/main/java/com/ody/mate/service/MateService.java +++ b/backend/src/main/java/com/ody/mate/service/MateService.java @@ -17,6 +17,8 @@ import com.ody.notification.service.NotificationService; import com.ody.route.domain.RouteTime; import com.ody.route.service.RouteService; +import com.ody.util.TimeUtil; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -27,6 +29,8 @@ @Transactional(readOnly = true) public class MateService { + private static final long AVAILABLE_NUDGE_DURATION = 30L; + private final MateRepository mateRepository; private final EtaService etaService; private final NotificationService notificationService; @@ -72,12 +76,32 @@ public List findAllByMeetingIdIfMate(Member member, long meetingId) { public void nudge(NudgeRequest nudgeRequest) { Mate requestMate = findFetchedMate(nudgeRequest.requestMateId()); Mate nudgedMate = findFetchedMate(nudgeRequest.nudgedMateId()); + validateNudgeCondition(requestMate, nudgedMate); + notificationService.sendNudgeMessage(requestMate, nudgedMate); + } + + private void validateNudgeCondition(Mate requestMate, Mate nudgedMate) { + if (!requestMate.isAttended(nudgedMate.getMeeting())) { + throw new OdyBadRequestException("재촉한 참여자가 같은 약속 참여자가 아닙니다"); + } - if (requestMate.isAttended(nudgedMate.getMeeting()) && canNudge(nudgedMate)) { - notificationService.sendNudgeMessage(requestMate, nudgedMate); - return; + if (!canNudgedStatus(nudgedMate)) { + throw new OdyBadRequestException("재촉한 참여자가 지각/지각위기가 아닙니다"); } - throw new OdyBadRequestException("재촉한 참여자가 같은 약속 참여자가 아니거나 지각/지각위기가 아닙니다"); + + if (!isWithinNudgeTime(nudgedMate.getMeeting())) { + throw new OdyBadRequestException("재촉할 수 있는 시간이 지난 요청입니다"); + } + } + + private boolean isWithinNudgeTime(Meeting meeting) { + LocalDateTime nudgeEndTime = meeting.getMeetingTime().plusMinutes(AVAILABLE_NUDGE_DURATION); + return !TimeUtil.nowWithTrim().isAfter(nudgeEndTime); + } + + private boolean canNudgedStatus(Mate mate) { + EtaStatus etaStatus = etaService.findEtaStatus(mate); + return etaStatus == EtaStatus.LATE_WARNING || etaStatus == EtaStatus.LATE; } private Mate findFetchedMate(Long mateId) { @@ -89,11 +113,6 @@ private Mate findFetchedMate(Long mateId) { return mate; } - private boolean canNudge(Mate mate) { - EtaStatus etaStatus = etaService.findEtaStatus(mate); - return etaStatus == EtaStatus.LATE_WARNING || etaStatus == EtaStatus.LATE; - } - @Transactional public MateEtaResponsesV2 findAllMateEtas(MateEtaRequest mateEtaRequest, Long meetingId, Member member) { Mate mate = findByMeetingIdAndMemberId(meetingId, member.getId()); diff --git a/backend/src/main/java/com/ody/route/config/RouteClientLoggingInterceptor.java b/backend/src/main/java/com/ody/route/config/RouteClientLoggingInterceptor.java new file mode 100644 index 000000000..3bf2553e0 --- /dev/null +++ b/backend/src/main/java/com/ody/route/config/RouteClientLoggingInterceptor.java @@ -0,0 +1,37 @@ +package com.ody.route.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.StreamUtils; + +@Slf4j +@RequiredArgsConstructor +public class RouteClientLoggingInterceptor implements ClientHttpRequestInterceptor { + + private final ObjectMapper objectMapper; + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) + throws IOException { + log.info("[RouteClient Request] Method: {}, URI: {}", request.getMethod(), request.getURI()); + + ClientHttpResponse response = execution.execute(request, body); + + String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); + String singleLineBody = convertToSingleLine(responseBody); + log.info("[RouteClient Response] Status: {}, Body: {}", response.getStatusCode(), singleLineBody); + return response; + } + + private String convertToSingleLine(String jsonString) throws IOException { + Object json = objectMapper.readValue(jsonString, Object.class); + return objectMapper.writeValueAsString(json); + } +} diff --git a/backend/src/main/java/com/ody/route/config/RouteClientProperties.java b/backend/src/main/java/com/ody/route/config/RouteClientProperties.java new file mode 100644 index 000000000..c4171b2c1 --- /dev/null +++ b/backend/src/main/java/com/ody/route/config/RouteClientProperties.java @@ -0,0 +1,23 @@ +package com.ody.route.config; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@ConfigurationProperties(prefix = "route") +public class RouteClientProperties { + + private final Map properties; + + public RouteClientProperties(List vendors) { + properties = vendors.stream() + .collect(Collectors.toMap(RouteClientProperty::name, property -> property)); + } + + public RouteClientProperty getProperty(String name) { + return properties.get(name); + } +} diff --git a/backend/src/main/java/com/ody/route/config/RouteClientProperty.java b/backend/src/main/java/com/ody/route/config/RouteClientProperty.java new file mode 100644 index 000000000..e293023f4 --- /dev/null +++ b/backend/src/main/java/com/ody/route/config/RouteClientProperty.java @@ -0,0 +1,5 @@ +package com.ody.route.config; + +public record RouteClientProperty(String name, String baseUrl, String apiKey) { + +} diff --git a/backend/src/main/java/com/ody/route/config/RouteConfig.java b/backend/src/main/java/com/ody/route/config/RouteConfig.java index b96b7c43f..4f79d524f 100644 --- a/backend/src/main/java/com/ody/route/config/RouteConfig.java +++ b/backend/src/main/java/com/ody/route/config/RouteConfig.java @@ -1,55 +1,58 @@ package com.ody.route.config; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ody.route.service.GoogleRouteClient; import com.ody.route.service.OdsayRouteClient; import com.ody.route.service.RouteClient; import java.time.Duration; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; -import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.core.annotation.Order; +import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.web.client.RestClient; @Profile("!test") @Configuration @RequiredArgsConstructor -@EnableConfigurationProperties(RouteProperties.class) +@EnableConfigurationProperties(RouteClientProperties.class) public class RouteConfig { - private final RouteProperties routeProperties; + private static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.ofSeconds(60); + private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(30); + + private final RouteClientProperties properties; @Bean @Order(1) - public RouteClient odysayRouteClient(RestClient.Builder routeRestClientBuilder) { - return new OdsayRouteClient(routeProperties, routeRestClientBuilder); + public RouteClient odysayRouteClient(ObjectMapper objectMapper) { + RouteClientProperty property = properties.getProperty("odsay"); + return new OdsayRouteClient(property, builder(objectMapper)); } @Bean @Order(2) - public RouteClient googleRouteClient( - RestClient.Builder routeRestClientBuilder, - @Value("${google.maps.api-key}") String googleApiKey - ) { - return new GoogleRouteClient(routeRestClientBuilder, googleApiKey); + public RouteClient googleRouteClient(ObjectMapper objectMapper) { + RouteClientProperty property = properties.getProperty("google"); + return new GoogleRouteClient(property, builder(objectMapper)); } @Bean - public RestClientCustomizer routeRestClientCustomizer() { - return builder -> builder.requestFactory(clientHttpRequestFactory()) - .build(); + public RestClient.Builder builder(ObjectMapper objectMapper) { + return RestClient.builder() + .requestFactory(new BufferingClientHttpRequestFactory(clientHttpRequestFactory())) + .requestInterceptor(new RouteClientLoggingInterceptor(objectMapper)); } private ClientHttpRequestFactory clientHttpRequestFactory() { ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS - .withConnectTimeout(Duration.ofSeconds(60)) - .withReadTimeout(Duration.ofSeconds(30)); //TODO: timeout 처리 로직 구현 + .withConnectTimeout(DEFAULT_CONNECTION_TIMEOUT) + .withReadTimeout(DEFAULT_READ_TIMEOUT); return ClientHttpRequestFactories.get(settings); } } diff --git a/backend/src/main/java/com/ody/route/service/GoogleRouteClient.java b/backend/src/main/java/com/ody/route/service/GoogleRouteClient.java index afa49216e..9c9d65da6 100644 --- a/backend/src/main/java/com/ody/route/service/GoogleRouteClient.java +++ b/backend/src/main/java/com/ody/route/service/GoogleRouteClient.java @@ -3,6 +3,7 @@ import com.ody.common.exception.OdyBadRequestException; import com.ody.common.exception.OdyServerErrorException; import com.ody.meeting.domain.Coordinates; +import com.ody.route.config.RouteClientProperty; import com.ody.route.domain.ClientType; import com.ody.route.domain.RouteTime; import com.ody.route.dto.DistanceMatrixElementStatus; @@ -10,19 +11,20 @@ import com.ody.route.dto.DistanceMatrixResponse.DistanceMatrixElement; import com.ody.route.dto.DistanceMatrixStatus; import java.time.Duration; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.client.RestClient; import org.springframework.web.util.UriComponentsBuilder; @Slf4j +@RequiredArgsConstructor public class GoogleRouteClient implements RouteClient { - private final String apiKey; + private final RouteClientProperty property; private final RestClient restClient; - public GoogleRouteClient(RestClient.Builder routeRestClientBuilder, String apiKey) { - this.apiKey = apiKey; - this.restClient = routeRestClientBuilder.baseUrl("https://maps.googleapis.com").build(); + public GoogleRouteClient(RouteClientProperty property, RestClient.Builder builder) { + this(property, builder.build()); } @Override @@ -33,12 +35,12 @@ public RouteTime calculateRouteTime(Coordinates origin, Coordinates target) { } private DistanceMatrixResponse getDistanceMatrixResponse(Coordinates origin, Coordinates target) { - String url = UriComponentsBuilder.fromPath("/maps/api/distancematrix/json") + String url = UriComponentsBuilder.fromHttpUrl(property.baseUrl()) .queryParam("destinations", mapCoordinatesToUrl(target)) .queryParam("origins", mapCoordinatesToUrl(origin)) .queryParam("mode", "transit") .queryParam("transit_mode", "bus|subway") - .queryParam("key", apiKey) + .queryParam("key", property.apiKey()) .build(false) .toUriString(); diff --git a/backend/src/main/java/com/ody/route/service/OdsayRouteClient.java b/backend/src/main/java/com/ody/route/service/OdsayRouteClient.java index e3f08bc2b..63cdb0256 100644 --- a/backend/src/main/java/com/ody/route/service/OdsayRouteClient.java +++ b/backend/src/main/java/com/ody/route/service/OdsayRouteClient.java @@ -2,7 +2,7 @@ import com.ody.common.exception.OdyServerErrorException; import com.ody.meeting.domain.Coordinates; -import com.ody.route.config.RouteProperties; +import com.ody.route.config.RouteClientProperty; import com.ody.route.domain.ClientType; import com.ody.route.domain.RouteTime; import com.ody.route.dto.OdsayResponse; @@ -10,21 +10,19 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Objects; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.client.RestClient; @Slf4j +@RequiredArgsConstructor public class OdsayRouteClient implements RouteClient { - private final RouteProperties routeProperties; + private final RouteClientProperty property; private final RestClient restClient; - public OdsayRouteClient( - RouteProperties routeProperties, - RestClient.Builder routeRestClientBuilder - ) { - this.routeProperties = routeProperties; - this.restClient = routeRestClientBuilder.build(); + public OdsayRouteClient(RouteClientProperty property, RestClient.Builder builder) { + this(property, builder.build()); } @Override @@ -45,12 +43,12 @@ private OdsayResponse getOdsayResponse(Coordinates origin, Coordinates target) { } private URI makeURI(Coordinates origin, Coordinates target) { - String uri = routeProperties.getUrl() + String uri = property.baseUrl() + "?SX=" + origin.getLongitude() + "&SY=" + origin.getLatitude() + "&EX=" + target.getLongitude() + "&EY=" + target.getLatitude() - + "&apiKey=" + routeProperties.getApiKey(); + + "&apiKey=" + property.apiKey(); try { return new URI(uri); } catch (URISyntaxException exception) { 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 0f55c154f..13dd10152 100644 --- a/backend/src/main/java/com/ody/route/service/RouteService.java +++ b/backend/src/main/java/com/ody/route/service/RouteService.java @@ -6,6 +6,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; import org.springframework.stereotype.Service; @Slf4j @@ -29,7 +30,12 @@ public RouteTime calculateRouteTime(Coordinates origin, Coordinates target) { RouteTime routeTime = calculateTime(client, origin, target); apiCallService.increaseCountByClientType(client.getClientType()); - log.info("{}를 사용한 소요 시간 계산 성공", client.getClass().getSimpleName()); + log.info( + "mateId : {}, {} API 사용한 소요 시간 계산 : {}분", + MDC.get("mateId"), + client.getClientType(), + routeTime.getMinutes() + ); return routeTime; } catch (Exception exception) { log.warn("Route Client 에러 : {} ", client.getClass().getSimpleName(), exception); diff --git a/backend/src/main/resources/common.yml b/backend/src/main/resources/common.yml index ecffabcfb..50ae7880a 100644 --- a/backend/src/main/resources/common.yml +++ b/backend/src/main/resources/common.yml @@ -20,13 +20,14 @@ springdoc: operations-sorter: alpha enabled: true -odsay: - url: https://api.odsay.com/v1/api/searchPubTransPathT - api-key: ENC(7am2oBS6Y/CCfThV+Yv59ZpjjIu8vOxt8v0/cEvGFDfUemAmSpbz+vfR3bsoDrKoJ/EwmeV6VXo=) - -google: - maps: - api-key: ENC(B/qgWb19BUQN+qdCHQKP7Ocx4xszftoRM4DfcjSIxCXlwgpfvZW4QpFBMGbNqVsf) +route: + vendors: + - name: odsay + base-url: https://api.odsay.com/v1/api/searchPubTransPathT + api-key: ENC(7am2oBS6Y/CCfThV+Yv59ZpjjIu8vOxt8v0/cEvGFDfUemAmSpbz+vfR3bsoDrKoJ/EwmeV6VXo=) + - name: google + base-url: https://maps.googleapis.com/maps/api/distancematrix/json + api-key: ENC(B/qgWb19BUQN+qdCHQKP7Ocx4xszftoRM4DfcjSIxCXlwgpfvZW4QpFBMGbNqVsf) auth: access-key: ENC(Ve3QKOE7PzbTOpQ6b58Oyi1Qrr9DojPgUA+jCwYcmDdSVdP34Z2eKw==) diff --git a/backend/src/test/java/com/ody/common/BaseRouteClientTest.java b/backend/src/test/java/com/ody/common/BaseRouteClientTest.java index 524bc7b52..490b36c2d 100644 --- a/backend/src/test/java/com/ody/common/BaseRouteClientTest.java +++ b/backend/src/test/java/com/ody/common/BaseRouteClientTest.java @@ -1,6 +1,7 @@ package com.ody.common; -import com.ody.route.config.RouteProperties; +import com.ody.route.config.RouteClientProperties; +import com.ody.route.config.RouteClientProperty; import com.ody.route.service.RouteClient; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; @@ -8,7 +9,7 @@ import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestClient; -@EnableConfigurationProperties(RouteProperties.class) +@EnableConfigurationProperties(RouteClientProperties.class) public abstract class BaseRouteClientTest { @Autowired @@ -18,15 +19,20 @@ public abstract class BaseRouteClientTest { protected RestClient.Builder restClientBuilder; @Autowired - protected RouteProperties routeProperties; + protected RouteClientProperties properties; protected RouteClient routeClient; + protected RouteClientProperty property; + @BeforeEach void setUp() { this.mockServer = MockRestServiceServer.bindTo(restClientBuilder).build(); + this.property = getProperty(); this.routeClient = createRouteClient(); } + protected abstract RouteClientProperty getProperty(); + protected abstract RouteClient createRouteClient(); } 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 81499cbcc..1cb0c5cd0 100644 --- a/backend/src/test/java/com/ody/mate/service/MateServiceTest.java +++ b/backend/src/test/java/com/ody/mate/service/MateServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -143,6 +144,37 @@ void nudgeFailWhenArrived() { assertThatThrownBy(() -> mateService.nudge(nudgeRequest)) .isInstanceOf(OdyBadRequestException.class); } + + @DisplayName("약속 30분 이후까지 mate를 재촉할 수 있다") + @Test + void nudgeSuccessWhenTimeWithInNudgeAvailableTime() { + Meeting availableNudgeMeeting = fixtureGenerator.generateMeeting(LocalDateTime.now().minusMinutes(30L)); + Mate requestMate = fixtureGenerator.generateMate(availableNudgeMeeting); + Mate nudgedLateWarningMate = fixtureGenerator.generateMate(availableNudgeMeeting); + Eta lateWarningEta = fixtureGenerator.generateEta(nudgedLateWarningMate, 2L); + + NudgeRequest nudgeRequest = new NudgeRequest(requestMate.getId(), nudgedLateWarningMate.getId()); + mateService.nudge(nudgeRequest); + + Mockito.verify(fcmPushSender, times(1)).sendNudgeMessage(any(Notification.class), any(DirectMessage.class)); + } + + @DisplayName("약속 30분 이후에는 mate를 재촉할 수 없다") + @Test + void nudgeFailWhenTimeIsNotWithInNudgeAvailableTime() { + Meeting notAvailableNudgeMeeting = fixtureGenerator.generateMeeting(LocalDateTime.now().minusMinutes(31L)); + Mate requestMate = fixtureGenerator.generateMate(notAvailableNudgeMeeting); + Mate nudgedLateWarningMate = fixtureGenerator.generateMate(notAvailableNudgeMeeting); + Eta lateWarningEta = fixtureGenerator.generateEta(nudgedLateWarningMate, 2L); + + NudgeRequest nudgeRequest = new NudgeRequest(requestMate.getId(), nudgedLateWarningMate.getId()); + + assertAll( + () -> assertThatThrownBy(() -> mateService.nudge(nudgeRequest)) + .isInstanceOf(OdyBadRequestException.class), + () -> Mockito.verifyNoInteractions(fcmPushSender) + ); + } } @DisplayName("참여자 생성") diff --git a/backend/src/test/java/com/ody/route/service/GoogleRouteClientTest.java b/backend/src/test/java/com/ody/route/service/GoogleRouteClientTest.java index ddf6ca0b9..a1695dbb5 100644 --- a/backend/src/test/java/com/ody/route/service/GoogleRouteClientTest.java +++ b/backend/src/test/java/com/ody/route/service/GoogleRouteClientTest.java @@ -9,10 +9,10 @@ import com.ody.common.exception.OdyBadRequestException; import com.ody.common.exception.OdyServerErrorException; import com.ody.meeting.domain.Coordinates; +import com.ody.route.config.RouteClientProperty; import com.ody.route.domain.RouteTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -20,12 +20,7 @@ import org.springframework.web.util.UriComponentsBuilder; @RestClientTest(GoogleRouteClient.class) -public class GoogleRouteClientTest extends BaseRouteClientTest { - - private static final String BASE_URI = "https://maps.googleapis.com/maps/api/distancematrix/json?"; - - @Value("${google.maps.api-key}") - private String testApiKey; +class GoogleRouteClientTest extends BaseRouteClientTest { @DisplayName("버스, 지하철을 이용한 소요시간 계산 요청 성공 시, 가장 빠른 소요 시간을 분으로 변환하여 반환한다.") @Test @@ -110,12 +105,12 @@ void calculateRouteTimeWhenElementStatusNotOK() { } private String makeUri(Coordinates origin, Coordinates target) { - return UriComponentsBuilder.fromHttpUrl(BASE_URI) + return UriComponentsBuilder.fromHttpUrl(property.baseUrl()) .queryParam("destinations", mapCoordinatesToUrl(target)) .queryParam("origins", mapCoordinatesToUrl(origin)) .queryParam("mode", "transit") .queryParam("transit_mode", "bus%7Csubway") - .queryParam("key", testApiKey) + .queryParam("key", property.apiKey()) .build() .toUriString(); } @@ -130,8 +125,13 @@ private void setMockServer(Coordinates origin, Coordinates target, String respon .andRespond(MockRestResponseCreators.withSuccess(response, MediaType.APPLICATION_JSON)); } + @Override + protected RouteClientProperty getProperty() { + return properties.getProperty("google"); + } + @Override protected RouteClient createRouteClient() { - return new GoogleRouteClient(restClientBuilder, testApiKey); + return new GoogleRouteClient(property, restClientBuilder); } } 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 b0b5943d6..ff76aa575 100644 --- a/backend/src/test/java/com/ody/route/service/OdsayRouteClientTest.java +++ b/backend/src/test/java/com/ody/route/service/OdsayRouteClientTest.java @@ -10,6 +10,7 @@ import com.ody.common.exception.OdyBadRequestException; import com.ody.common.exception.OdyServerErrorException; import com.ody.meeting.domain.Coordinates; +import com.ody.route.config.RouteClientProperty; import com.ody.route.domain.RouteTime; import java.io.IOException; import java.net.URI; @@ -91,12 +92,12 @@ private void setMockServer(Coordinates origin, Coordinates target, String respon } private URI makeUri(Coordinates origin, Coordinates target) { - String uri = routeProperties.getUrl() + String uri = property.baseUrl() + "?SX=" + origin.getLongitude() + "&SY=" + origin.getLatitude() + "&EX=" + target.getLongitude() + "&EY=" + target.getLatitude() - + "&apiKey=" + routeProperties.getApiKey(); + + "&apiKey=" + property.apiKey(); try { return new URI(uri); @@ -111,8 +112,13 @@ private String makeResponseByPath(String path) throws IOException { ); } + @Override + protected RouteClientProperty getProperty() { + return properties.getProperty("odsay"); + } + @Override protected RouteClient createRouteClient() { - return new OdsayRouteClient(routeProperties, restClientBuilder); + return new OdsayRouteClient(property, restClientBuilder); } } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index cfc48e5c3..2c7bca394 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -14,13 +14,14 @@ spring: flyway: enabled: false -odsay: - url: https://api.odsay.com/v1/api/searchPubTransPathT - api-key: testApiKey - -google: - maps: - api-key: testApiKey +route: + vendors: + - name: odsay + base-url: https://api.odsay.com/v1/api/searchPubTransPathT + api-key: testApiKey + - name: google + base-url: https://maps.googleapis.com/maps/api/distancematrix/json + api-key: testApiKey allowed-origins: api-call: testOrigin