diff --git a/core/data/src/main/java/com/puzzle/data/repository/MatchingRepositoryImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/MatchingRepositoryImpl.kt index aa981041..ed89daa4 100644 --- a/core/data/src/main/java/com/puzzle/data/repository/MatchingRepositoryImpl.kt +++ b/core/data/src/main/java/com/puzzle/data/repository/MatchingRepositoryImpl.kt @@ -1,11 +1,26 @@ package com.puzzle.data.repository +import com.puzzle.common.suspendRunCatching +import com.puzzle.datastore.datasource.matching.LocalMatchingDataSource import com.puzzle.domain.model.match.MatchInfo +import com.puzzle.domain.model.profile.OpponentProfile +import com.puzzle.domain.model.profile.OpponentProfileBasic +import com.puzzle.domain.model.profile.OpponentValuePick +import com.puzzle.domain.model.profile.OpponentValueTalk import com.puzzle.domain.repository.MatchingRepository +import com.puzzle.network.model.matching.GetMatchInfoResponse +import com.puzzle.network.model.matching.GetOpponentProfileBasicResponse +import com.puzzle.network.model.matching.GetOpponentProfileImageResponse +import com.puzzle.network.model.matching.GetOpponentValuePicksResponse +import com.puzzle.network.model.matching.GetOpponentValueTalksResponse import com.puzzle.network.source.matching.MatchingDataSource +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.first import javax.inject.Inject class MatchingRepositoryImpl @Inject constructor( + private val localMatchingDataSource: LocalMatchingDataSource, private val matchingDataSource: MatchingDataSource, ) : MatchingRepository { override suspend fun reportUser(userId: Int, reason: String): Result = @@ -16,7 +31,57 @@ class MatchingRepositoryImpl @Inject constructor( matchingDataSource.blockContacts(phoneNumbers) override suspend fun getMatchInfo(): Result = matchingDataSource.getMatchInfo() - .mapCatching { response -> response.toDomain() } + .mapCatching(GetMatchInfoResponse::toDomain) + + override suspend fun retrieveOpponentProfile(): Result = suspendRunCatching { + localMatchingDataSource.opponentProfile.first() + } + + override suspend fun loadOpponentProfile(): Result = suspendRunCatching { + coroutineScope { + val valueTalksDeferred = async { getOpponentValueTalks() } + val valuePicksDeferred = async { getOpponentValuePicks() } + val profileBasicDeferred = async { getOpponentProfileBasic() } + val profileImageDeferred = async { getOpponentProfileImage() } + + val valuePicks = valuePicksDeferred.await().getOrThrow() + val valueTalks = valueTalksDeferred.await().getOrThrow() + val profileBasic = profileBasicDeferred.await().getOrThrow() + val imageUrl = profileImageDeferred.await().getOrThrow() + + val result = OpponentProfile( + description = profileBasic.description, + nickname = profileBasic.nickname, + age = profileBasic.age, + birthYear = profileBasic.birthYear, + height = profileBasic.height, + weight = profileBasic.weight, + location = profileBasic.location, + job = profileBasic.job, + smokingStatus = profileBasic.smokingStatus, + valuePicks = valuePicks, + valueTalks = valueTalks, + imageUrl = imageUrl, + ) + localMatchingDataSource.setOpponentProfile(result) + } + } + + private suspend fun getOpponentValueTalks(): Result> = + matchingDataSource.getOpponentValueTalks() + .mapCatching(GetOpponentValueTalksResponse::toDomain) + + private suspend fun getOpponentValuePicks(): Result> = + matchingDataSource.getOpponentValuePicks() + .mapCatching(GetOpponentValuePicksResponse::toDomain) + + private suspend fun getOpponentProfileBasic(): Result = + matchingDataSource.getOpponentProfileBasic() + .mapCatching(GetOpponentProfileBasicResponse::toDomain) + + private suspend fun getOpponentProfileImage(): Result = + matchingDataSource.getOpponentProfileImage() + .mapCatching(GetOpponentProfileImageResponse::toDomain) override suspend fun checkMatchingPiece(): Result = matchingDataSource.checkMatchingPiece() diff --git a/core/data/src/test/java/com/puzzle/data/fake/source/matching/FakeLocalMatchingDataSource.kt b/core/data/src/test/java/com/puzzle/data/fake/source/matching/FakeLocalMatchingDataSource.kt new file mode 100644 index 00000000..edcfd2e5 --- /dev/null +++ b/core/data/src/test/java/com/puzzle/data/fake/source/matching/FakeLocalMatchingDataSource.kt @@ -0,0 +1,21 @@ +package com.puzzle.data.fake.source.matching + +import com.puzzle.datastore.datasource.matching.LocalMatchingDataSource +import com.puzzle.domain.model.profile.OpponentProfile +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class FakeLocalMatchingDataSource : LocalMatchingDataSource { + private var storedOpponentProfile: OpponentProfile? = null + + override val opponentProfile: Flow = flow { + storedOpponentProfile?.let { emit(it) } + ?: throw NoSuchElementException("No value present in DataStore") + } + + override suspend fun setOpponentProfile(profile: OpponentProfile) { + storedOpponentProfile = profile + } + + override suspend fun clearOpponentProfile() {} +} diff --git a/core/data/src/test/java/com/puzzle/data/fake/source/matching/FakeMatchingDataSource.kt b/core/data/src/test/java/com/puzzle/data/fake/source/matching/FakeMatchingDataSource.kt new file mode 100644 index 00000000..42c89486 --- /dev/null +++ b/core/data/src/test/java/com/puzzle/data/fake/source/matching/FakeMatchingDataSource.kt @@ -0,0 +1,92 @@ +package com.puzzle.data.fake.source.matching + +import com.puzzle.network.model.matching.GetMatchInfoResponse +import com.puzzle.network.model.matching.GetOpponentProfileBasicResponse +import com.puzzle.network.model.matching.GetOpponentProfileImageResponse +import com.puzzle.network.model.matching.GetOpponentValuePicksResponse +import com.puzzle.network.model.matching.GetOpponentValueTalksResponse +import com.puzzle.network.source.matching.MatchingDataSource + +class FakeMatchingDataSource : MatchingDataSource { + private var opponentProfileBasicData: GetOpponentProfileBasicResponse? = null + private var opponentValuePicksData: GetOpponentValuePicksResponse? = null + private var opponentValueTalksData: GetOpponentValueTalksResponse? = null + private var opponentProfileImageData: GetOpponentProfileImageResponse? = null + + fun setOpponentProfileData( + basicData: GetOpponentProfileBasicResponse, + valuePicksData: GetOpponentValuePicksResponse, + valueTalksData: GetOpponentValueTalksResponse, + profileImageData: GetOpponentProfileImageResponse + ) { + opponentProfileBasicData = basicData + opponentValuePicksData = valuePicksData + opponentValueTalksData = valueTalksData + opponentProfileImageData = profileImageData + } + + override suspend fun getOpponentProfileBasic(): Result = + Result.success( + opponentProfileBasicData ?: GetOpponentProfileBasicResponse( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ) + ) + + override suspend fun getOpponentValuePicks(): Result = + Result.success( + opponentValuePicksData ?: GetOpponentValuePicksResponse( + null, + null, + null, + null + ) + ) + + override suspend fun getOpponentValueTalks(): Result = + Result.success( + opponentValueTalksData ?: GetOpponentValueTalksResponse( + null, + null, + null, + null + ) + ) + + override suspend fun getOpponentProfileImage(): Result = + Result.success(opponentProfileImageData ?: GetOpponentProfileImageResponse(null)) + + override suspend fun reportUser(userId: Int, reason: String): Result = + Result.success(Unit) + + override suspend fun blockUser(userId: Int): Result = Result.success(Unit) + + override suspend fun blockContacts(phoneNumbers: List): Result = + Result.success(Unit) + + override suspend fun getMatchInfo(): Result = Result.success( + GetMatchInfoResponse( + matchId = 1234, + matchStatus = "WAITING", + description = "안녕하세요, 저는 음악과 여행을 좋아하는 사람입니다.", + nickname = "여행하는음악가", + birthYear = "1995", + location = "서울특별시", + job = "음악 프로듀서", + matchedValueCount = 3, + matchedValueList = listOf("음악", "여행", "독서") + ) + ) + + override suspend fun checkMatchingPiece(): Result = Result.success(Unit) + + override suspend fun acceptMatching(): Result = Result.success(Unit) +} diff --git a/core/data/src/test/java/com/puzzle/data/repository/MatchingRepositoryImplTest.kt b/core/data/src/test/java/com/puzzle/data/repository/MatchingRepositoryImplTest.kt new file mode 100644 index 00000000..5edfc821 --- /dev/null +++ b/core/data/src/test/java/com/puzzle/data/repository/MatchingRepositoryImplTest.kt @@ -0,0 +1,117 @@ +package com.puzzle.data.repository + +import com.puzzle.data.fake.source.matching.FakeLocalMatchingDataSource +import com.puzzle.data.fake.source.matching.FakeMatchingDataSource +import com.puzzle.domain.model.profile.OpponentProfile +import com.puzzle.domain.model.profile.OpponentValuePick +import com.puzzle.domain.model.profile.OpponentValueTalk +import com.puzzle.network.model.matching.GetOpponentProfileBasicResponse +import com.puzzle.network.model.matching.GetOpponentProfileImageResponse +import com.puzzle.network.model.matching.GetOpponentValuePicksResponse +import com.puzzle.network.model.matching.GetOpponentValueTalksResponse +import com.puzzle.network.model.matching.OpponentValuePickResponse +import com.puzzle.network.model.matching.OpponentValueTalkResponse +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class MatchingRepositoryImplTest { + + private lateinit var matchingRepository: MatchingRepositoryImpl + private lateinit var fakeMatchingDataSource: FakeMatchingDataSource + private lateinit var fakeLocalMatchingDataSource: FakeLocalMatchingDataSource + + @BeforeEach + fun setUp() { + fakeMatchingDataSource = FakeMatchingDataSource() + fakeLocalMatchingDataSource = FakeLocalMatchingDataSource() + matchingRepository = MatchingRepositoryImpl( + localMatchingDataSource = fakeLocalMatchingDataSource, + matchingDataSource = fakeMatchingDataSource + ) + } + + @Test + fun `매칭된 상대방 정보는 로컬에 저장한다`() = runTest { + // Given + val expectedOpponentProfile = OpponentProfile( + description = "안녕하세요", + nickname = "테스트", + age = 25, + birthYear = "1998", + height = 170, + weight = 60, + location = "서울", + job = "개발자", + smokingStatus = "비흡연", + valuePicks = listOf( + OpponentValuePick( + category = "취미", + question = "당신의 취미는 무엇인가요?", + isSameWithMe = true, + answerOptions = emptyList(), + selectedAnswer = 1 + ) + ), + valueTalks = listOf( + OpponentValueTalk( + category = "음악", + summary = "좋아하는 음악 장르", + answer = "저는 클래식 음악을 좋아합니다." + ) + ), + imageUrl = "https://example.com/image.jpg" + ) + + fakeMatchingDataSource.setOpponentProfileData( + GetOpponentProfileBasicResponse( + matchId = 1, + description = expectedOpponentProfile.description, + nickname = expectedOpponentProfile.nickname, + age = expectedOpponentProfile.age, + birthYear = expectedOpponentProfile.birthYear, + height = expectedOpponentProfile.height, + weight = expectedOpponentProfile.weight, + location = expectedOpponentProfile.location, + job = expectedOpponentProfile.job, + smokingStatus = expectedOpponentProfile.smokingStatus + ), + GetOpponentValuePicksResponse( + matchId = 1, + description = null, + nickname = null, + valuePicks = expectedOpponentProfile.valuePicks.map { + OpponentValuePickResponse( + category = it.category, + question = it.question, + isSameWithMe = it.isSameWithMe, + answerOptions = null, + selectedAnswer = it.selectedAnswer + ) + } + ), + GetOpponentValueTalksResponse( + matchId = 1, + description = null, + nickname = null, + valueTalks = expectedOpponentProfile.valueTalks.map { + OpponentValueTalkResponse( + category = it.category, + summary = it.summary, + answer = it.answer + ) + } + ), + GetOpponentProfileImageResponse(imageUrl = expectedOpponentProfile.imageUrl) + ) + + // When + matchingRepository.loadOpponentProfile().getOrThrow() + + // Then + val storedProfile = fakeLocalMatchingDataSource.opponentProfile.first() + assertEquals(expectedOpponentProfile, storedProfile) + } +} diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index 3f005841..563b6ce6 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -8,5 +8,8 @@ android { } dependencies { + implementation(projects.core.domain) + implementation(libs.androidx.datastore) + implementation(libs.gson) } diff --git a/core/datastore/src/main/java/com/puzzle/datastore/datasource/matching/LocalMatchingDataSource.kt b/core/datastore/src/main/java/com/puzzle/datastore/datasource/matching/LocalMatchingDataSource.kt new file mode 100644 index 00000000..dee8bb61 --- /dev/null +++ b/core/datastore/src/main/java/com/puzzle/datastore/datasource/matching/LocalMatchingDataSource.kt @@ -0,0 +1,10 @@ +package com.puzzle.datastore.datasource.matching + +import com.puzzle.domain.model.profile.OpponentProfile +import kotlinx.coroutines.flow.Flow + +interface LocalMatchingDataSource { + val opponentProfile: Flow + suspend fun setOpponentProfile(opponentProfile: OpponentProfile) + suspend fun clearOpponentProfile() +} diff --git a/core/datastore/src/main/java/com/puzzle/datastore/datasource/matching/LocalMatchingDataSourceImpl.kt b/core/datastore/src/main/java/com/puzzle/datastore/datasource/matching/LocalMatchingDataSourceImpl.kt new file mode 100644 index 00000000..ef7527b4 --- /dev/null +++ b/core/datastore/src/main/java/com/puzzle/datastore/datasource/matching/LocalMatchingDataSourceImpl.kt @@ -0,0 +1,35 @@ +package com.puzzle.datastore.datasource.matching + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.google.gson.Gson +import com.puzzle.datastore.util.getValue +import com.puzzle.datastore.util.setValue +import com.puzzle.domain.model.profile.OpponentProfile +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Named + +class LocalMatchingDataSourceImpl @Inject constructor( + @Named("matching") private val dataStore: DataStore, +) : LocalMatchingDataSource { + private val gson = Gson() + override val opponentProfile: Flow = dataStore.getValue(OPPONENT_PROFILE, "") + .map { gson.fromJson(it, OpponentProfile::class.java) } + + override suspend fun setOpponentProfile(opponentProfile: OpponentProfile) { + val jsonString = gson.toJson(opponentProfile) + dataStore.setValue(OPPONENT_PROFILE, jsonString) + } + + override suspend fun clearOpponentProfile() { + dataStore.edit { preferences -> preferences.remove(OPPONENT_PROFILE) } + } + + companion object { + private val OPPONENT_PROFILE = stringPreferencesKey("OPPONENT_PROFILE") + } +} diff --git a/core/datastore/src/main/java/com/puzzle/datastore/di/DataStoreModule.kt b/core/datastore/src/main/java/com/puzzle/datastore/di/DataStoreModule.kt index 68bda38b..bf76b4ad 100644 --- a/core/datastore/src/main/java/com/puzzle/datastore/di/DataStoreModule.kt +++ b/core/datastore/src/main/java/com/puzzle/datastore/di/DataStoreModule.kt @@ -4,6 +4,8 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore +import com.puzzle.datastore.datasource.matching.LocalMatchingDataSource +import com.puzzle.datastore.datasource.matching.LocalMatchingDataSourceImpl import com.puzzle.datastore.datasource.token.LocalTokenDataSource import com.puzzle.datastore.datasource.token.LocalTokenDataSourceImpl import com.puzzle.datastore.datasource.user.LocalUserDataSource @@ -26,6 +28,9 @@ object DataStoreProvidesModule { private const val USER_DATASTORE_NAME = "USERS_PREFERENCES" private val Context.userDataStore by preferencesDataStore(name = USER_DATASTORE_NAME) + private const val MATCHING_DATASTORE_NAME = "MATCHING_PREFERENCES" + private val Context.matchingDataStore by preferencesDataStore(name = MATCHING_DATASTORE_NAME) + @Provides @Singleton @Named("token") @@ -39,6 +44,13 @@ object DataStoreProvidesModule { fun provideUserDataStore( @ApplicationContext context: Context ): DataStore = context.userDataStore + + @Provides + @Singleton + @Named("matching") + fun provideMatchingDataStore( + @ApplicationContext context: Context + ): DataStore = context.matchingDataStore } @Module @@ -47,7 +59,7 @@ abstract class DatastoreBindsModule { @Binds @Singleton - abstract fun bindsLocalMatchingDataSource( + abstract fun bindsLocalTokenDataSource( localTokenDataSourceImpl: LocalTokenDataSourceImpl, ): LocalTokenDataSource @@ -56,4 +68,10 @@ abstract class DatastoreBindsModule { abstract fun bindsLocalUserDataSource( localUserDataSourceImpl: LocalUserDataSourceImpl, ): LocalUserDataSource + + @Binds + @Singleton + abstract fun bindsLocalMatchingDataSource( + localMatchingDataSourceImpl: LocalMatchingDataSourceImpl, + ): LocalMatchingDataSource } diff --git a/core/datastore/src/main/java/com/puzzle/datastore/util/DataStoreUtil.kt b/core/datastore/src/main/java/com/puzzle/datastore/util/DataStoreUtil.kt index 4924c5ea..85257ba3 100644 --- a/core/datastore/src/main/java/com/puzzle/datastore/util/DataStoreUtil.kt +++ b/core/datastore/src/main/java/com/puzzle/datastore/util/DataStoreUtil.kt @@ -15,22 +15,16 @@ internal fun DataStore.getValue( defaultValue: T ): Flow = data.handleException() - .map { preferences -> - preferences[key] ?: defaultValue - } + .map { preferences -> preferences[key] ?: defaultValue } internal suspend fun DataStore.setValue( key: Preferences.Key, value: T, -) = edit { preferences -> - preferences[key] = value -} +) = edit { preferences -> preferences[key] = value } internal suspend fun DataStore.clear( key: Preferences.Key -) = edit { preferences -> - preferences.remove(key) -} +) = edit { preferences -> preferences.remove(key) } private fun Flow.handleException(): Flow = this.catch { exception -> diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Loading.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Loading.kt new file mode 100644 index 00000000..80c5efe6 --- /dev/null +++ b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Loading.kt @@ -0,0 +1,8 @@ +package com.puzzle.designsystem.component + +import androidx.compose.runtime.Composable + +@Composable +fun PieceLoading() { + // TODO +} diff --git a/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentProfile.kt b/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentProfile.kt new file mode 100644 index 00000000..20b9b4e2 --- /dev/null +++ b/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentProfile.kt @@ -0,0 +1,42 @@ +package com.puzzle.domain.model.profile + +data class OpponentProfile( + val description: String, + val nickname: String, + val age: Int, + val birthYear: String, + val height: Int, + val weight: Int, + val location: String, + val job: String, + val smokingStatus: String, + val valuePicks: List, + val valueTalks: List, + val imageUrl: String, +) + +data class OpponentProfileBasic( + val description: String, + val nickname: String, + val age: Int, + val birthYear: String, + val height: Int, + val weight: Int, + val location: String, + val job: String, + val smokingStatus: String, +) + +data class OpponentValuePick( + val category: String, + val question: String, + val answerOptions: List, + val selectedAnswer: Int, + val isSameWithMe: Boolean, +) + +data class OpponentValueTalk( + val category: String, + val summary: String, + val answer: String, +) diff --git a/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentValuePick.kt b/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentValuePick.kt deleted file mode 100644 index 6b1561c1..00000000 --- a/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentValuePick.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.puzzle.domain.model.profile - -data class OpponentValuePick( - val category: String, - val question: String, - val answerOptions: List, - val selectedAnswer: Int, - val isSameWithMe: Boolean, -) diff --git a/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentValueTalk.kt b/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentValueTalk.kt deleted file mode 100644 index f284b564..00000000 --- a/core/domain/src/main/java/com/puzzle/domain/model/profile/OpponentValueTalk.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.puzzle.domain.model.profile - -data class OpponentValueTalk( - val category: String, - val summary: String, - val answer: String, -) diff --git a/core/domain/src/main/java/com/puzzle/domain/repository/MatchingRepository.kt b/core/domain/src/main/java/com/puzzle/domain/repository/MatchingRepository.kt index a2a10ffb..aff864b3 100644 --- a/core/domain/src/main/java/com/puzzle/domain/repository/MatchingRepository.kt +++ b/core/domain/src/main/java/com/puzzle/domain/repository/MatchingRepository.kt @@ -1,12 +1,15 @@ package com.puzzle.domain.repository import com.puzzle.domain.model.match.MatchInfo +import com.puzzle.domain.model.profile.OpponentProfile interface MatchingRepository { suspend fun reportUser(userId: Int, reason: String): Result suspend fun blockUser(userId: Int): Result suspend fun blockContacts(phoneNumbers: List): Result suspend fun getMatchInfo(): Result + suspend fun retrieveOpponentProfile(): Result + suspend fun loadOpponentProfile(): Result suspend fun checkMatchingPiece(): Result suspend fun acceptMatching(): Result } diff --git a/core/domain/src/main/java/com/puzzle/domain/usecase/matching/GetOpponentProfileUseCase.kt b/core/domain/src/main/java/com/puzzle/domain/usecase/matching/GetOpponentProfileUseCase.kt new file mode 100644 index 00000000..4e82390f --- /dev/null +++ b/core/domain/src/main/java/com/puzzle/domain/usecase/matching/GetOpponentProfileUseCase.kt @@ -0,0 +1,16 @@ +package com.puzzle.domain.usecase.matching + +import com.puzzle.domain.model.profile.OpponentProfile +import com.puzzle.domain.repository.MatchingRepository +import javax.inject.Inject + +class GetOpponentProfileUseCase @Inject constructor( + private val matchingRepository: MatchingRepository, +) { + suspend operator fun invoke(): Result = + matchingRepository.retrieveOpponentProfile() + .recoverCatching { + matchingRepository.loadOpponentProfile().getOrThrow() + matchingRepository.retrieveOpponentProfile().getOrThrow() + } +} diff --git a/core/domain/src/test/kotlin/com/puzzle/domain/spy/repository/SpyMatchingRepository.kt b/core/domain/src/test/kotlin/com/puzzle/domain/spy/repository/SpyMatchingRepository.kt new file mode 100644 index 00000000..51c8769e --- /dev/null +++ b/core/domain/src/test/kotlin/com/puzzle/domain/spy/repository/SpyMatchingRepository.kt @@ -0,0 +1,64 @@ +package com.puzzle.domain.spy.repository + +import com.puzzle.domain.model.match.MatchInfo +import com.puzzle.domain.model.profile.OpponentProfile +import com.puzzle.domain.repository.MatchingRepository + +class SpyMatchingRepository : MatchingRepository { + private var localOpponentProfile: OpponentProfile? = null + private var remoteOpponentProfile: OpponentProfile? = null + private var shouldFailLocalRetrieval = false + var loadOpponentProfileCallCount = 0 + private set + + fun setLocalOpponentProfile(profile: OpponentProfile?) { + localOpponentProfile = profile + } + + fun setRemoteOpponentProfile(profile: OpponentProfile) { + remoteOpponentProfile = profile + } + + fun setShouldFailLocalRetrieval(shouldFail: Boolean) { + shouldFailLocalRetrieval = shouldFail + } + + override suspend fun retrieveOpponentProfile(): Result = + if (shouldFailLocalRetrieval) { + Result.failure(NoSuchElementException("No value present in DataStore")) + } else { + localOpponentProfile?.let { Result.success(it) } + ?: Result.failure(NoSuchElementException("No value present in DataStore")) + } + + override suspend fun loadOpponentProfile(): Result { + loadOpponentProfileCallCount++ + shouldFailLocalRetrieval = false + localOpponentProfile = remoteOpponentProfile + return Result.success(Unit) + } + + override suspend fun reportUser(userId: Int, reason: String): Result { + TODO("Not yet implemented") + } + + override suspend fun blockUser(userId: Int): Result { + TODO("Not yet implemented") + } + + override suspend fun blockContacts(phoneNumbers: List): Result { + TODO("Not yet implemented") + } + + override suspend fun getMatchInfo(): Result { + TODO("Not yet implemented") + } + + override suspend fun checkMatchingPiece(): Result { + TODO("Not yet implemented") + } + + override suspend fun acceptMatching(): Result { + TODO("Not yet implemented") + } +} diff --git a/core/domain/src/test/kotlin/com/puzzle/domain/usecase/matching/GetOpponentProfileUseCaseTest.kt b/core/domain/src/test/kotlin/com/puzzle/domain/usecase/matching/GetOpponentProfileUseCaseTest.kt new file mode 100644 index 00000000..0d85f6ac --- /dev/null +++ b/core/domain/src/test/kotlin/com/puzzle/domain/usecase/matching/GetOpponentProfileUseCaseTest.kt @@ -0,0 +1,76 @@ +import com.puzzle.domain.model.profile.OpponentProfile +import com.puzzle.domain.spy.repository.SpyMatchingRepository +import com.puzzle.domain.usecase.matching.GetOpponentProfileUseCase +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class GetOpponentProfileUseCaseTest { + private lateinit var spyMatchingRepository: SpyMatchingRepository + private lateinit var getOpponentProfileUseCase: GetOpponentProfileUseCase + + @BeforeEach + fun setup() { + spyMatchingRepository = SpyMatchingRepository() + getOpponentProfileUseCase = GetOpponentProfileUseCase(spyMatchingRepository) + } + + @Test + fun `로컬에서 데이터를 불러오다가 실패했을 경우 서버 데이터를 불러온다`() = runTest { + // Given + val localProfile = OpponentProfile( + description = "Local Profile", + nickname = "LocalUser", + age = 25, + birthYear = "1998", + height = 170, + weight = 65, + location = "Seoul", + job = "Developer", + smokingStatus = "Non-smoker", + valuePicks = emptyList(), + valueTalks = emptyList(), + imageUrl = "local_image_url" + ) + val remoteProfile = localProfile.copy(description = "Remote Profile") + + spyMatchingRepository.setShouldFailLocalRetrieval(true) + spyMatchingRepository.setRemoteOpponentProfile(remoteProfile) + + // When + val result = getOpponentProfileUseCase() + + // Then + assertEquals(remoteProfile, result.getOrNull()) + assertEquals(1, spyMatchingRepository.loadOpponentProfileCallCount) + } + + @Test + fun `로컬에서 데이터를 성공적으로 불러올 경우 서버 요청을 하지 않는다`() = runTest { + // Given + val localProfile = OpponentProfile( + description = "Local Profile", + nickname = "LocalUser", + age = 25, + birthYear = "1998", + height = 170, + weight = 65, + location = "Seoul", + job = "Developer", + smokingStatus = "Non-smoker", + valuePicks = emptyList(), + valueTalks = emptyList(), + imageUrl = "local_image_url" + ) + + spyMatchingRepository.setLocalOpponentProfile(localProfile) + + // When + val result = getOpponentProfileUseCase() + + // Then + assertEquals(localProfile, result.getOrNull()) + assertEquals(0, spyMatchingRepository.loadOpponentProfileCallCount) + } +} diff --git a/core/network/src/main/java/com/puzzle/network/api/PieceApi.kt b/core/network/src/main/java/com/puzzle/network/api/PieceApi.kt index 3cd0f45f..85ee07b1 100644 --- a/core/network/src/main/java/com/puzzle/network/api/PieceApi.kt +++ b/core/network/src/main/java/com/puzzle/network/api/PieceApi.kt @@ -8,6 +8,10 @@ import com.puzzle.network.model.auth.VerifyAuthCodeRequest import com.puzzle.network.model.auth.VerifyAuthCodeResponse import com.puzzle.network.model.matching.BlockContactsRequest import com.puzzle.network.model.matching.GetMatchInfoResponse +import com.puzzle.network.model.matching.GetOpponentProfileBasicResponse +import com.puzzle.network.model.matching.GetOpponentProfileImageResponse +import com.puzzle.network.model.matching.GetOpponentValuePicksResponse +import com.puzzle.network.model.matching.GetOpponentValueTalksResponse import com.puzzle.network.model.matching.LoadValuePicksResponse import com.puzzle.network.model.matching.LoadValueTalksResponse import com.puzzle.network.model.matching.ReportUserRequest @@ -20,8 +24,8 @@ import com.puzzle.network.model.token.RefreshTokenResponse import okhttp3.MultipartBody import retrofit2.http.Body import retrofit2.http.GET -import retrofit2.http.PATCH import retrofit2.http.Multipart +import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Part import retrofit2.http.Path @@ -77,6 +81,18 @@ interface PieceApi { @GET("/api/matches/infos") suspend fun getMatchInfo(): Result> + @GET("/api/matches/values/talks") + suspend fun getOpponentValueTalks(): Result> + + @GET("/api/matches/values/picks") + suspend fun getOpponentValuePicks(): Result> + + @GET("/api/matches/profiles/basic") + suspend fun getOpponentProfileBasic(): Result> + + @GET("/api/matches/images") + suspend fun getOpponentProfileImage(): Result> + @PATCH("/api/matches/pieces/check") suspend fun checkMatchingPiece(): Result> diff --git a/core/network/src/main/java/com/puzzle/network/di/NetworkModule.kt b/core/network/src/main/java/com/puzzle/network/di/NetworkModule.kt index d82b7ae1..ecb840b6 100644 --- a/core/network/src/main/java/com/puzzle/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/puzzle/network/di/NetworkModule.kt @@ -2,6 +2,8 @@ package com.puzzle.network.di import com.puzzle.network.source.auth.AuthDataSource import com.puzzle.network.source.auth.AuthDataSourceImpl +import com.puzzle.network.source.matching.MatchingDataSource +import com.puzzle.network.source.matching.MatchingDataSourceImpl import com.puzzle.network.source.profile.ProfileDataSource import com.puzzle.network.source.profile.ProfileDataSourceImpl import com.puzzle.network.source.term.TermDataSource @@ -24,8 +26,8 @@ abstract class NetworkModule { @Binds @Singleton - abstract fun bindsMatchingDataSource( - matchingDataSourceImpl: ProfileDataSourceImpl, + abstract fun bindsProfileDataSource( + profileDataSourceImpl: ProfileDataSourceImpl, ): ProfileDataSource @Binds @@ -33,4 +35,10 @@ abstract class NetworkModule { abstract fun bindsTermDataSource( termDataSourceImpl: TermDataSourceImpl, ): TermDataSource + + @Binds + @Singleton + abstract fun bindsMatchingDataSource( + matchingDataSourceImpl: MatchingDataSourceImpl, + ): MatchingDataSource } diff --git a/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentProfileBasicResponse.kt b/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentProfileBasicResponse.kt new file mode 100644 index 00000000..7e6661c5 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentProfileBasicResponse.kt @@ -0,0 +1,32 @@ +package com.puzzle.network.model.matching + +import com.puzzle.domain.model.profile.OpponentProfileBasic +import com.puzzle.network.model.UNKNOWN_INT +import com.puzzle.network.model.UNKNOWN_STRING +import kotlinx.serialization.Serializable + +@Serializable +data class GetOpponentProfileBasicResponse( + val matchId: Int?, + val description: String?, + val nickname: String?, + val age: Int?, + val birthYear: String?, + val height: Int?, + val weight: Int?, + val location: String?, + val job: String?, + val smokingStatus: String?, +) { + fun toDomain() = OpponentProfileBasic( + description = description ?: UNKNOWN_STRING, + nickname = nickname ?: UNKNOWN_STRING, + age = age ?: UNKNOWN_INT, + birthYear = birthYear ?: UNKNOWN_STRING, + height = height ?: UNKNOWN_INT, + weight = weight ?: UNKNOWN_INT, + location = location ?: UNKNOWN_STRING, + job = job ?: UNKNOWN_STRING, + smokingStatus = smokingStatus ?: UNKNOWN_STRING, + ) +} diff --git a/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentProfileImageResponse.kt b/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentProfileImageResponse.kt new file mode 100644 index 00000000..8f4669c5 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentProfileImageResponse.kt @@ -0,0 +1,11 @@ +package com.puzzle.network.model.matching + +import com.puzzle.network.model.UNKNOWN_STRING +import kotlinx.serialization.Serializable + +@Serializable +data class GetOpponentProfileImageResponse( + val imageUrl: String?, +) { + fun toDomain(): String = imageUrl ?: UNKNOWN_STRING +} diff --git a/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentValuePicksResponse.kt b/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentValuePicksResponse.kt new file mode 100644 index 00000000..3dfad065 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentValuePicksResponse.kt @@ -0,0 +1,34 @@ +package com.puzzle.network.model.matching + +import com.puzzle.domain.model.profile.OpponentValuePick +import com.puzzle.network.model.UNKNOWN_INT +import com.puzzle.network.model.UNKNOWN_STRING +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetOpponentValuePicksResponse( + val matchId: Int?, + val description: String?, + val nickname: String?, + val valuePicks: List?, +) { + fun toDomain() = valuePicks?.map(OpponentValuePickResponse::toDomain) ?: emptyList() +} + +@Serializable +data class OpponentValuePickResponse( + val category: String?, + val question: String?, + @SerialName("sameWithMe") val isSameWithMe: Boolean?, + @SerialName("answer") val answerOptions: List?, + @SerialName("answerNumber") val selectedAnswer: Int?, +) { + fun toDomain() = OpponentValuePick( + category = category ?: UNKNOWN_STRING, + question = question ?: UNKNOWN_STRING, + isSameWithMe = isSameWithMe ?: false, + answerOptions = answerOptions?.map(ValuePickAnswerResponse::toDomain) ?: emptyList(), + selectedAnswer = selectedAnswer ?: UNKNOWN_INT, + ) +} diff --git a/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentValueTalksResponse.kt b/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentValueTalksResponse.kt new file mode 100644 index 00000000..24ffc655 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/matching/GetOpponentValueTalksResponse.kt @@ -0,0 +1,28 @@ +package com.puzzle.network.model.matching + +import com.puzzle.domain.model.profile.OpponentValueTalk +import com.puzzle.network.model.UNKNOWN_STRING +import kotlinx.serialization.Serializable + +@Serializable +data class GetOpponentValueTalksResponse( + val matchId: Int?, + val description: String?, + val nickname: String?, + val valueTalks: List?, +) { + fun toDomain() = valueTalks?.map(OpponentValueTalkResponse::toDomain) ?: emptyList() +} + +@Serializable +data class OpponentValueTalkResponse( + val category: String?, + val summary: String?, + val answer: String?, +) { + fun toDomain() = OpponentValueTalk( + category = category ?: UNKNOWN_STRING, + summary = summary ?: UNKNOWN_STRING, + answer = answer ?: UNKNOWN_STRING, + ) +} diff --git a/core/network/src/main/java/com/puzzle/network/source/matching/MatchingDataSource.kt b/core/network/src/main/java/com/puzzle/network/source/matching/MatchingDataSource.kt index cb987efd..78631b26 100644 --- a/core/network/src/main/java/com/puzzle/network/source/matching/MatchingDataSource.kt +++ b/core/network/src/main/java/com/puzzle/network/source/matching/MatchingDataSource.kt @@ -1,26 +1,20 @@ package com.puzzle.network.source.matching -import com.puzzle.network.api.PieceApi -import com.puzzle.network.model.matching.BlockContactsRequest import com.puzzle.network.model.matching.GetMatchInfoResponse -import com.puzzle.network.model.matching.ReportUserRequest -import com.puzzle.network.model.unwrapData -import javax.inject.Inject -import javax.inject.Singleton +import com.puzzle.network.model.matching.GetOpponentProfileBasicResponse +import com.puzzle.network.model.matching.GetOpponentProfileImageResponse +import com.puzzle.network.model.matching.GetOpponentValuePicksResponse +import com.puzzle.network.model.matching.GetOpponentValueTalksResponse -@Singleton -class MatchingDataSource @Inject constructor( - private val pieceApi: PieceApi -) { - suspend fun reportUser(userId: Int, reason: String): Result = - pieceApi.reportUser(ReportUserRequest(userId = userId, reason = reason)).unwrapData() - - suspend fun blockUser(userId: Int): Result = pieceApi.blockUser(userId).unwrapData() - suspend fun blockContacts(phoneNumbers: List): Result = - pieceApi.blockContacts(BlockContactsRequest(phoneNumbers)).unwrapData() - - suspend fun getMatchInfo(): Result = pieceApi.getMatchInfo().unwrapData() - - suspend fun checkMatchingPiece(): Result = pieceApi.checkMatchingPiece().unwrapData() - suspend fun acceptMatching(): Result = pieceApi.acceptMatching().unwrapData() +interface MatchingDataSource { + suspend fun reportUser(userId: Int, reason: String): Result + suspend fun blockUser(userId: Int): Result + suspend fun blockContacts(phoneNumbers: List): Result + suspend fun getMatchInfo(): Result + suspend fun getOpponentValueTalks(): Result + suspend fun getOpponentValuePicks(): Result + suspend fun getOpponentProfileBasic(): Result + suspend fun getOpponentProfileImage(): Result + suspend fun checkMatchingPiece(): Result + suspend fun acceptMatching(): Result } diff --git a/core/network/src/main/java/com/puzzle/network/source/matching/MatchingDataSourceImpl.kt b/core/network/src/main/java/com/puzzle/network/source/matching/MatchingDataSourceImpl.kt new file mode 100644 index 00000000..e627a546 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/source/matching/MatchingDataSourceImpl.kt @@ -0,0 +1,47 @@ +package com.puzzle.network.source.matching + +import com.puzzle.network.api.PieceApi +import com.puzzle.network.model.matching.BlockContactsRequest +import com.puzzle.network.model.matching.GetMatchInfoResponse +import com.puzzle.network.model.matching.GetOpponentProfileBasicResponse +import com.puzzle.network.model.matching.GetOpponentProfileImageResponse +import com.puzzle.network.model.matching.GetOpponentValuePicksResponse +import com.puzzle.network.model.matching.GetOpponentValueTalksResponse +import com.puzzle.network.model.matching.ReportUserRequest +import com.puzzle.network.model.unwrapData +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MatchingDataSourceImpl @Inject constructor( + private val pieceApi: PieceApi +) : MatchingDataSource { + override suspend fun reportUser(userId: Int, reason: String): Result = + pieceApi.reportUser(ReportUserRequest(userId = userId, reason = reason)).unwrapData() + + override suspend fun blockUser(userId: Int): Result = + pieceApi.blockUser(userId).unwrapData() + + override suspend fun blockContacts(phoneNumbers: List): Result = + pieceApi.blockContacts(BlockContactsRequest(phoneNumbers)).unwrapData() + + override suspend fun getMatchInfo(): Result = + pieceApi.getMatchInfo().unwrapData() + + override suspend fun getOpponentValueTalks(): Result = + pieceApi.getOpponentValueTalks().unwrapData() + + override suspend fun getOpponentValuePicks(): Result = + pieceApi.getOpponentValuePicks().unwrapData() + + override suspend fun getOpponentProfileBasic(): Result = + pieceApi.getOpponentProfileBasic().unwrapData() + + override suspend fun getOpponentProfileImage(): Result = + pieceApi.getOpponentProfileImage().unwrapData() + + override suspend fun checkMatchingPiece(): Result = + pieceApi.checkMatchingPiece().unwrapData() + + override suspend fun acceptMatching(): Result = pieceApi.acceptMatching().unwrapData() +} diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt index 85220700..6b80856e 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailScreen.kt @@ -42,9 +42,11 @@ import com.puzzle.designsystem.component.PieceDialog import com.puzzle.designsystem.component.PieceDialogBottom import com.puzzle.designsystem.component.PieceDialogDefaultTop import com.puzzle.designsystem.component.PieceImageDialog +import com.puzzle.designsystem.component.PieceLoading import com.puzzle.designsystem.component.PieceRoundingButton import com.puzzle.designsystem.component.PieceSubCloseTopBar import com.puzzle.designsystem.foundation.PieceTheme +import com.puzzle.domain.model.profile.OpponentProfile import com.puzzle.matching.graph.detail.bottomsheet.MatchingDetailMoreBottomSheet import com.puzzle.matching.graph.detail.common.constant.DialogType import com.puzzle.matching.graph.detail.contract.MatchingDetailIntent @@ -167,7 +169,7 @@ private fun MatchingDetailScreen( DialogType.PROFILE_IMAGE_DETAIL -> { PieceImageDialog( - imageUri = state.imageUri, + imageUri = state.profile?.imageUrl, buttonLabel = "매칭 수락하기", onButtonClick = { dialogType = DialogType.ACCEPT_MATCHING }, onDismissRequest = { showDialog = false }, @@ -216,13 +218,13 @@ private fun MatchingDetailScreen( .fillMaxWidth() .height(topBarHeight) .align(Alignment.TopCenter) - .let { + .then( if (state.currentPage != MatchingDetailPage.BasicInfoPage) { - it.background(PieceTheme.colors.white) + Modifier.background(PieceTheme.colors.white) } else { - it + Modifier } - } + ) .padding(horizontal = 20.dp), ) @@ -254,10 +256,7 @@ private fun MatchingDetailScreen( @Composable private fun BackgroundImage(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxSize() - ) { + Box(modifier = modifier.fillMaxSize()) { Image( painter = painterResource(id = R.drawable.matchingdetail_bg), contentDescription = "basic info 배경화면", @@ -275,44 +274,47 @@ private fun MatchingDetailContent( modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxSize()) { - AnimatedContent( - targetState = state.currentPage, - transitionSpec = { - fadeIn(tween(700)) togetherWith fadeOut(tween(700)) - }, - modifier = Modifier.fillMaxSize(), - ) { - when (it) { - MatchingDetailState.MatchingDetailPage.BasicInfoPage -> - BasicInfoPage( - nickName = state.nickName, - selfDescription = state.selfDescription, - birthYear = state.birthYear, - age = state.age, - height = state.height, - activityRegion = state.activityRegion, - occupation = state.occupation, - smokeStatue = state.smokeStatue, - onMoreClick = onMoreClick, - ) + state.profile?.let { profile -> + AnimatedContent( + targetState = state.currentPage, + transitionSpec = { + fadeIn(tween(700)) togetherWith fadeOut(tween(700)) + }, + modifier = Modifier.fillMaxSize(), + ) { + when (it) { + MatchingDetailPage.BasicInfoPage -> + BasicInfoPage( + nickName = profile.nickname, + selfDescription = profile.description, + birthYear = profile.birthYear, + age = profile.age, + height = profile.height, + weight = profile.weight, + activityRegion = profile.location, + occupation = profile.job, + smokingStatus = profile.smokingStatus, + onMoreClick = onMoreClick, + ) - MatchingDetailState.MatchingDetailPage.ValueTalkPage -> - ValueTalkPage( - nickName = state.nickName, - selfDescription = state.selfDescription, - talkCards = state.talkCards, - onMoreClick = onMoreClick, - ) + MatchingDetailPage.ValueTalkPage -> + ValueTalkPage( + nickName = profile.nickname, + selfDescription = profile.description, + talkCards = profile.valueTalks, + onMoreClick = onMoreClick, + ) - MatchingDetailState.MatchingDetailPage.ValuePickPage -> - ValuePickPage( - nickName = state.nickName, - selfDescription = state.selfDescription, - pickCards = state.pickCards, - onDeclineClick = onDeclineClick, - ) + MatchingDetailPage.ValuePickPage -> + ValuePickPage( + nickName = profile.nickname, + selfDescription = profile.description, + pickCards = profile.valuePicks, + onDeclineClick = onDeclineClick, + ) + } } - } + } ?: PieceLoading() } } @@ -386,7 +388,22 @@ private fun MatchingDetailBottomBar( private fun MatchingDetailScreenPreview() { PieceTheme { MatchingDetailScreen( - MatchingDetailState(), + MatchingDetailState( + profile = OpponentProfile( + description = "음악과 요리를 좋아하는", + nickname = "수줍은 수달", + birthYear = "00", + age = 25, + height = 254, + weight = 72, + job = "개발자", + location = "서울특별시", + smokingStatus = "비흡연", + valueTalks = emptyList(), + valuePicks = emptyList(), + imageUrl = "", + ) + ), {}, {}, {}, diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt index d3240633..cdc86a8e 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/MatchingDetailViewModel.kt @@ -9,6 +9,7 @@ import com.puzzle.common.event.EventHelper import com.puzzle.common.event.PieceEvent import com.puzzle.domain.model.error.ErrorHelper import com.puzzle.domain.repository.MatchingRepository +import com.puzzle.domain.usecase.matching.GetOpponentProfileUseCase import com.puzzle.matching.graph.detail.contract.MatchingDetailIntent import com.puzzle.matching.graph.detail.contract.MatchingDetailSideEffect import com.puzzle.matching.graph.detail.contract.MatchingDetailState @@ -27,6 +28,7 @@ import kotlinx.coroutines.launch class MatchingDetailViewModel @AssistedInject constructor( @Assisted initialState: MatchingDetailState, + private val getOpponentProfileUseCase: GetOpponentProfileUseCase, private val matchingRepository: MatchingRepository, internal val navigationHelper: NavigationHelper, private val eventHelper: EventHelper, @@ -39,19 +41,35 @@ class MatchingDetailViewModel @AssistedInject constructor( private val matchUserId = 0 // Todo 임시 init { + initMatchDetailInfo() + intents.receiveAsFlow() .onEach(::processIntent) .launchIn(viewModelScope) } + private fun initMatchDetailInfo() = viewModelScope.launch { + setState { copy(isLoading = true) } + + getOpponentProfileUseCase().onSuccess { response -> + setState { copy(profile = response) } + }.onFailure { + errorHelper.sendError(it) + }.also { + setState { copy(isLoading = false) } + } + } + internal fun onIntent(intent: MatchingDetailIntent) = viewModelScope.launch { intents.send(intent) } - private suspend fun processIntent(intent: MatchingDetailIntent) { + private fun processIntent(intent: MatchingDetailIntent) { when (intent) { is MatchingDetailIntent.OnMoreClick -> showBottomSheet(intent.content) - MatchingDetailIntent.OnMatchingDetailCloseClick -> navigateTo(NavigationEvent.NavigateUp) + MatchingDetailIntent.OnMatchingDetailCloseClick -> + navigationHelper.navigate(NavigationEvent.NavigateUp) + MatchingDetailIntent.OnPreviousPageClick -> setPreviousPage() MatchingDetailIntent.OnNextPageClick -> setNextPage() MatchingDetailIntent.OnBlockClick -> onBlockClick() @@ -74,11 +92,11 @@ class MatchingDetailViewModel @AssistedInject constructor( private fun onBlockClick() { withState { - navigateTo( + navigationHelper.navigate( NavigationEvent.NavigateTo( MatchingGraphDest.BlockRoute( userId = matchUserId, - userName = it.nickName, + userName = it.profile!!.nickname, ) ) ) @@ -89,11 +107,11 @@ class MatchingDetailViewModel @AssistedInject constructor( private fun onReportClick() { withState { - navigateTo( + navigationHelper.navigate( NavigationEvent.NavigateTo( MatchingGraphDest.ReportRoute( userId = matchUserId, - userName = it.nickName, + userName = it.profile!!.nickname, ) ) ) @@ -115,10 +133,6 @@ class MatchingDetailViewModel @AssistedInject constructor( .onFailure { errorHelper.sendError(it) } } - private fun navigateTo(navigationEvent: NavigationEvent) { - _sideEffects.trySend(MatchingDetailSideEffect.Navigate(navigationEvent)) - } - private fun showBottomSheet(content: @Composable () -> Unit) { eventHelper.sendEvent(PieceEvent.ShowBottomSheet(content)) } diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/contract/MatchingDetailState.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/contract/MatchingDetailState.kt index 88f37d62..7fb06d33 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/contract/MatchingDetailState.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/contract/MatchingDetailState.kt @@ -1,23 +1,12 @@ package com.puzzle.matching.graph.detail.contract import com.airbnb.mvrx.MavericksState -import com.puzzle.domain.model.profile.OpponentValuePick -import com.puzzle.domain.model.profile.OpponentValueTalk +import com.puzzle.domain.model.profile.OpponentProfile data class MatchingDetailState( val isLoading: Boolean = false, val currentPage: MatchingDetailPage = MatchingDetailPage.BasicInfoPage, - val selfDescription: String = "음악과 요리를 좋아하는", - val nickName: String = "수줍은 수달", - val age: String = "25", - val birthYear: String = "00", - val height: String = "254", - val occupation: String = "개발자", - val activityRegion: String = "서울특별시", - val smokeStatue: String = "비흡연", - val talkCards: List = emptyList(), - val pickCards: List = emptyList(), - val imageUri: String = "", + val profile: OpponentProfile? = null, ) : MavericksState { enum class MatchingDetailPage(val title: String) { diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/page/BasicInfoPage.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/page/BasicInfoPage.kt index 66b5ed6e..d781034f 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/page/BasicInfoPage.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/page/BasicInfoPage.kt @@ -30,11 +30,12 @@ internal fun BasicInfoPage( nickName: String, selfDescription: String, birthYear: String, - age: String, - height: String, + age: Int, + height: Int, + weight: Int, activityRegion: String, occupation: String, - smokeStatue: String, + smokingStatus: String, onMoreClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -52,9 +53,10 @@ internal fun BasicInfoPage( age = age, birthYear = birthYear, height = height, + weight = weight, activityRegion = activityRegion, occupation = occupation, - smokeStatue = smokeStatue, + smokeStatue = smokingStatus, modifier = Modifier.padding(horizontal = 20.dp) ) } @@ -85,10 +87,11 @@ private fun BasicInfoName( } @Composable -private fun ColumnScope.BasicInfoCard( - age: String, +private fun BasicInfoCard( + age: Int, birthYear: String, - height: String, + height: Int, + weight: Int, activityRegion: String, occupation: String, smokeStatue: String, @@ -113,7 +116,7 @@ private fun ColumnScope.BasicInfoCard( Spacer(modifier = Modifier.width(4.dp)) Text( - text = age, + text = age.toString(), style = PieceTheme.typography.headingSSB, color = PieceTheme.colors.black, ) @@ -145,7 +148,7 @@ private fun ColumnScope.BasicInfoCard( text = { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = height, + text = height.toString(), style = PieceTheme.typography.headingSSB, color = PieceTheme.colors.black, ) @@ -165,7 +168,7 @@ private fun ColumnScope.BasicInfoCard( text = { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "72", + text = weight.toString(), style = PieceTheme.typography.headingSSB, color = PieceTheme.colors.black, ) @@ -256,11 +259,12 @@ private fun ProfileBasicInfoPagePreview() { nickName = "수줍은 수달", selfDescription = "음악과 요리를 좋아하는", birthYear = "1994", - age = "31", - height = "200", + age = 31, + height = 200, + weight = 72, activityRegion = "서울특별시", occupation = "개발자", - smokeStatue = "비흡연", + smokingStatus = "비흡연", onMoreClick = { }, ) } diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt index 867b186d..6e237bee 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/main/MatchingViewModel.kt @@ -66,7 +66,13 @@ class MatchingViewModel @AssistedInject constructor( } private suspend fun getMatchInfo() = matchingRepository.getMatchInfo() - .onSuccess { setState { copy(matchInfo = it) } } + .onSuccess { + setState { copy(matchInfo = it) } + + // MatchingHome화면에서 사전에 MatchingDetail에서 필요한 데이터를 케싱해놓습니다. + matchingRepository.loadOpponentProfile() + .onFailure { errorHelper.sendError(it) } + } .onFailure { errorHelper.sendError(it) } private fun processOnButtonClick() = withState { state -> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5379dfda..7ce48b87 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,8 @@ kotlin = "2.1.0" kotlinxSerializationJson = "1.7.0" # https://github.com/Kotlin/kotlinx.coroutines kotlinxCoroutine = "1.9.0-RC" +# https://github.com/google/gson +gson = "2.12.1" ## Test # https://github.com/junit-team/junit4 @@ -150,6 +152,7 @@ okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-kotlin-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" } junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junitJupiter" }