diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index e12d91e1..ca1bc318 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -10,7 +10,6 @@ android { dependencies { implementation(projects.core.domain) implementation(projects.core.network) - implementation(projects.core.database) implementation(projects.core.datastore) implementation(projects.core.common) } diff --git a/core/data/src/main/java/com/puzzle/data/di/DataModule.kt b/core/data/src/main/java/com/puzzle/data/di/DataModule.kt index c3d8b130..76969433 100644 --- a/core/data/src/main/java/com/puzzle/data/di/DataModule.kt +++ b/core/data/src/main/java/com/puzzle/data/di/DataModule.kt @@ -1,6 +1,6 @@ package com.puzzle.data.di -import com.puzzle.data.TokenManagerImpl +import com.puzzle.data.repository.TokenManagerImpl import com.puzzle.data.image.ImageResizer import com.puzzle.data.image.ImageResizerImpl import com.puzzle.data.repository.AuthRepositoryImpl diff --git a/core/data/src/main/java/com/puzzle/data/repository/ProfileRepositoryImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/ProfileRepositoryImpl.kt index b58c3746..9c8b813c 100644 --- a/core/data/src/main/java/com/puzzle/data/repository/ProfileRepositoryImpl.kt +++ b/core/data/src/main/java/com/puzzle/data/repository/ProfileRepositoryImpl.kt @@ -2,14 +2,14 @@ package com.puzzle.data.repository import com.puzzle.common.suspendRunCatching import com.puzzle.data.image.ImageResizer -import com.puzzle.database.model.matching.ValuePickAnswerEntity -import com.puzzle.database.model.matching.ValuePickEntity -import com.puzzle.database.model.matching.ValuePickQuestionEntity -import com.puzzle.database.model.matching.ValueTalkEntity -import com.puzzle.database.source.profile.LocalProfileDataSource +import com.puzzle.datastore.datasource.profile.LocalProfileDataSource import com.puzzle.datastore.datasource.token.LocalTokenDataSource import com.puzzle.datastore.datasource.user.LocalUserDataSource import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.domain.model.profile.ValuePickAnswer import com.puzzle.domain.model.profile.ValuePickQuestion import com.puzzle.domain.model.profile.ValueTalkAnswer import com.puzzle.domain.model.profile.ValueTalkQuestion @@ -17,6 +17,7 @@ import com.puzzle.domain.repository.ProfileRepository import com.puzzle.network.model.UNKNOWN_INT import com.puzzle.network.source.profile.ProfileDataSource import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -28,61 +29,116 @@ class ProfileRepositoryImpl @Inject constructor( private val localUserDataSource: LocalUserDataSource, ) : ProfileRepository { override suspend fun loadValuePickQuestions(): Result = suspendRunCatching { - val valuePicks = profileDataSource.loadValuePickQuestions() + val valuePickQuestions = profileDataSource.loadValuePickQuestions() .getOrThrow() .toDomain() .filter { it.id != UNKNOWN_INT } - val valuePickEntities = valuePicks.map { valuePick -> - ValuePickEntity( - valuePickQuestion = ValuePickQuestionEntity( - id = valuePick.id, - category = valuePick.category, - question = valuePick.question, - ), - answers = valuePick.answerOptions.map { answer -> - ValuePickAnswerEntity( - questionsId = valuePick.id, - number = answer.number, - content = answer.content, - ) - } - ) - } - - localProfileDataSource.replaceValuePickQuestions(valuePickEntities) + localProfileDataSource.setValuePickQuestions(valuePickQuestions) } override suspend fun loadValueTalkQuestions(): Result = suspendRunCatching { - val valueTalks = profileDataSource.loadValueTalkQuestions() + val valueTalkQuestions = profileDataSource.loadValueTalkQuestions() .getOrThrow() .toDomain() .filter { it.id != UNKNOWN_INT } - val valueTalkEntities = valueTalks.map { - ValueTalkEntity( - id = it.id, - title = it.title, - category = it.category, - helpMessages = it.guides, - ) - } - - localProfileDataSource.replaceValueTalkQuestions(valueTalkEntities) + localProfileDataSource.setValueTalkQuestions(valueTalkQuestions) } override suspend fun retrieveValuePickQuestion(): Result> = + suspendRunCatching { localProfileDataSource.valuePickQuestions.first() } + + override suspend fun retrieveValueTalkQuestion(): Result> = + suspendRunCatching { localProfileDataSource.valueTalkQuestions.first() } + + override suspend fun retrieveMyProfileBasic(): Result = + suspendRunCatching { localProfileDataSource.myProfileBasic.first() } + + override suspend fun loadMyValueTalks(): Result = suspendRunCatching { + val valueTalks = profileDataSource.getMyValueTalks() + .getOrThrow() + .toDomain() + + localProfileDataSource.setMyValueTalks(valueTalks) + } + + override suspend fun retrieveMyValueTalks(): Result> = + suspendRunCatching { localProfileDataSource.myValueTalks.first() } + + override suspend fun loadMyValuePicks(): Result = suspendRunCatching { + val valuePicks = profileDataSource.getMyValuePicks() + .getOrThrow() + .toDomain() + + localProfileDataSource.setMyValuePicks(valuePicks) + } + + override suspend fun retrieveMyValuePicks(): Result> = + suspendRunCatching { localProfileDataSource.myValuePicks.first() } + + override suspend fun loadMyProfileBasic(): Result = suspendRunCatching { + val profileBasic = profileDataSource.getMyProfileBasic() + .getOrThrow() + .toDomain() + + localProfileDataSource.setMyProfileBasic(profileBasic) + } + + override suspend fun updateMyValueTalks(valueTalks: List): Result> = suspendRunCatching { - localProfileDataSource.retrieveValuePickQuestions() - .map(ValuePickEntity::toDomain) + val updatedValueTalks = + profileDataSource.updateMyValueTalks(valueTalks) + .getOrThrow() + .toDomain() + + localProfileDataSource.setMyValueTalks(updatedValueTalks) + updatedValueTalks } - override suspend fun retrieveValueTalkQuestion(): Result> = + override suspend fun updateMyValuePicks(valuePicks: List): Result> = suspendRunCatching { - localProfileDataSource.retrieveValueTalkQuestions() - .map(ValueTalkEntity::toDomain) + val updatedValuePicks = + profileDataSource.updateMyValuePicks(valuePicks) + .getOrThrow() + .toDomain() + + localProfileDataSource.setMyValuePicks(updatedValuePicks) + updatedValuePicks } + override suspend fun updateMyProfileBasic( + description: String, + nickname: String, + birthDate: String, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, + snsActivityLevel: String, + imageUrl: String, + contacts: List + ): Result = suspendRunCatching { + val updatedProfileBasic = profileDataSource.updateMyProfileBasic( + description = description, + nickname = nickname, + birthDate = birthDate, + height = height, + weight = weight, + location = location, + job = job, + smokingStatus = smokingStatus, + snsActivityLevel = snsActivityLevel, + imageUrl = imageUrl, + contacts = contacts + ).getOrThrow() + .toDomain() + + localProfileDataSource.setMyProfileBasic(updatedProfileBasic) + updatedProfileBasic + } + override suspend fun checkNickname(nickname: String): Result = profileDataSource.checkNickname(nickname) @@ -98,7 +154,7 @@ class ProfileRepositoryImpl @Inject constructor( smokingStatus: String, snsActivityLevel: String, contacts: List, - valuePicks: List, + valuePicks: List, valueTalks: List ): Result = suspendRunCatching { val uploadedImageUrl = diff --git a/core/data/src/main/java/com/puzzle/data/repository/TermsRepositoryImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/TermsRepositoryImpl.kt index 1622203d..a53e7969 100644 --- a/core/data/src/main/java/com/puzzle/data/repository/TermsRepositoryImpl.kt +++ b/core/data/src/main/java/com/puzzle/data/repository/TermsRepositoryImpl.kt @@ -1,12 +1,12 @@ package com.puzzle.data.repository import com.puzzle.common.suspendRunCatching -import com.puzzle.database.model.terms.TermEntity -import com.puzzle.database.source.term.LocalTermDataSource +import com.puzzle.datastore.datasource.term.LocalTermDataSource import com.puzzle.domain.model.terms.Term import com.puzzle.domain.repository.TermsRepository import com.puzzle.network.model.UNKNOWN_INT import com.puzzle.network.source.term.TermDataSource +import kotlinx.coroutines.flow.first import javax.inject.Inject class TermsRepositoryImpl @Inject constructor( @@ -19,22 +19,11 @@ class TermsRepositoryImpl @Inject constructor( .toDomain() .filter { it.id != UNKNOWN_INT } - val termsEntity = terms.map { - TermEntity( - id = it.id, - title = it.title, - content = it.content, - required = it.required, - startDate = it.startDate, - ) - } - - localTermDataSource.replaceTerms(termsEntity) + localTermDataSource.setTerms(terms) } override suspend fun retrieveTerms(): Result> = suspendRunCatching { - localTermDataSource.retrieveTerms() - .map(TermEntity::toDomain) + localTermDataSource.terms.first() } override suspend fun agreeTerms(ids: List): Result = termDataSource.agreeTerms(ids) diff --git a/core/data/src/main/java/com/puzzle/data/TokenManagerImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/TokenManagerImpl.kt similarity index 95% rename from core/data/src/main/java/com/puzzle/data/TokenManagerImpl.kt rename to core/data/src/main/java/com/puzzle/data/repository/TokenManagerImpl.kt index 7a280a48..a9c7c832 100644 --- a/core/data/src/main/java/com/puzzle/data/TokenManagerImpl.kt +++ b/core/data/src/main/java/com/puzzle/data/repository/TokenManagerImpl.kt @@ -1,4 +1,4 @@ -package com.puzzle.data +package com.puzzle.data.repository import com.puzzle.datastore.datasource.token.LocalTokenDataSource import com.puzzle.network.interceptor.TokenManager diff --git a/core/data/src/test/java/com/puzzle/data/fake/source/profile/FakeLocalProfileDataSource.kt b/core/data/src/test/java/com/puzzle/data/fake/source/profile/FakeLocalProfileDataSource.kt index ac6386af..0dbc3c15 100644 --- a/core/data/src/test/java/com/puzzle/data/fake/source/profile/FakeLocalProfileDataSource.kt +++ b/core/data/src/test/java/com/puzzle/data/fake/source/profile/FakeLocalProfileDataSource.kt @@ -1,28 +1,67 @@ package com.puzzle.data.fake.source.profile -import com.puzzle.database.model.matching.ValuePickEntity -import com.puzzle.database.model.matching.ValueTalkEntity -import com.puzzle.database.source.profile.LocalProfileDataSource +import com.puzzle.datastore.datasource.profile.LocalProfileDataSource +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.domain.model.profile.ValuePickQuestion +import com.puzzle.domain.model.profile.ValueTalkQuestion +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flow class FakeLocalProfileDataSource : LocalProfileDataSource { - private var valuePicks = listOf() - private var valueTalks = listOf() + private var _valuePickQuestions: List? = null + override val valuePickQuestions: Flow> + get() = flow { + _valuePickQuestions?.let { emit(it) } + ?: throw NoSuchElementException("No value present in DataStore") + } - override suspend fun retrieveValuePickQuestions(): List { - return valuePicks + private var _valueTalkQuestions: List? = null + override val valueTalkQuestions: Flow> + get() = flow { + _valueTalkQuestions?.let { emit(it) } + ?: throw NoSuchElementException("No value present in DataStore") + } + + private var _myProfileBasic: MyProfileBasic? = null + override val myProfileBasic: Flow + get() = flow { + _myProfileBasic?.let { emit(it) } + ?: throw NoSuchElementException("No value present in DataStore") + } + + private val _myValuePicks = MutableStateFlow>(emptyList()) + override val myValuePicks: Flow> = _myValuePicks.asStateFlow() + + private val _myValueTalks = MutableStateFlow>(emptyList()) + override val myValueTalks: Flow> = _myValueTalks.asStateFlow() + + override suspend fun setValuePickQuestions(valuePicks: List) { + _valuePickQuestions = valuePicks + } + + override suspend fun setValueTalkQuestions(valueTalks: List) { + _valueTalkQuestions = valueTalks + } + + override suspend fun setMyProfileBasic(profileBasic: MyProfileBasic) { + _myProfileBasic = profileBasic } - override suspend fun replaceValuePickQuestions(valuePicks: List): Result { - this.valuePicks = valuePicks - return Result.success(Unit) + override suspend fun setMyValuePicks(valuePicks: List) { + _myValuePicks.value = valuePicks } - override suspend fun retrieveValueTalkQuestions(): List { - return valueTalks + override suspend fun setMyValueTalks(valueTalks: List) { + _myValueTalks.value = valueTalks } - override suspend fun replaceValueTalkQuestions(valueTalks: List): Result { - this.valueTalks = valueTalks - return Result.success(Unit) + override suspend fun clearMyProfile() { + _myProfileBasic = null + _myValuePicks.value = emptyList() + _myValueTalks.value = emptyList() } } diff --git a/core/data/src/test/java/com/puzzle/data/fake/source/profile/FakeProfileDataSource.kt b/core/data/src/test/java/com/puzzle/data/fake/source/profile/FakeProfileDataSource.kt index 52680fee..eab3f24b 100644 --- a/core/data/src/test/java/com/puzzle/data/fake/source/profile/FakeProfileDataSource.kt +++ b/core/data/src/test/java/com/puzzle/data/fake/source/profile/FakeProfileDataSource.kt @@ -1,13 +1,22 @@ package com.puzzle.data.fake.source.profile import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk import com.puzzle.domain.model.profile.ValuePickAnswer import com.puzzle.domain.model.profile.ValueTalkAnswer -import com.puzzle.network.model.matching.LoadValuePicksResponse -import com.puzzle.network.model.matching.LoadValueTalksResponse -import com.puzzle.network.model.matching.ValuePickResponse -import com.puzzle.network.model.matching.ValueTalkResponse +import com.puzzle.network.model.profile.ContactResponse +import com.puzzle.network.model.profile.GetMyProfileBasicResponse +import com.puzzle.network.model.profile.GetMyValuePicksResponse +import com.puzzle.network.model.profile.GetMyValueTalksResponse +import com.puzzle.network.model.profile.LoadValuePickQuestionsResponse +import com.puzzle.network.model.profile.LoadValueTalkQuestionsResponse +import com.puzzle.network.model.profile.MyValuePickResponse +import com.puzzle.network.model.profile.MyValueTalkResponse import com.puzzle.network.model.profile.UploadProfileResponse +import com.puzzle.network.model.profile.ValuePickAnswerResponse +import com.puzzle.network.model.profile.ValuePickResponse +import com.puzzle.network.model.profile.ValueTalkResponse import com.puzzle.network.source.profile.ProfileDataSource import java.io.InputStream @@ -15,11 +24,94 @@ class FakeProfileDataSource : ProfileDataSource { private var valuePicks = listOf() private var valueTalks = listOf() - override suspend fun loadValuePickQuestions(): Result = - Result.success(LoadValuePicksResponse(responses = valuePicks)) + override suspend fun loadValuePickQuestions(): Result = + Result.success(LoadValuePickQuestionsResponse(responses = valuePicks)) - override suspend fun loadValueTalkQuestions(): Result = - Result.success(LoadValueTalksResponse(responses = valueTalks)) + override suspend fun loadValueTalkQuestions(): Result = + Result.success(LoadValueTalkQuestionsResponse(responses = valueTalks)) + + override suspend fun getMyProfileBasic(): Result { + TODO("Not yet implemented") + } + + override suspend fun getMyValueTalks(): Result { + TODO("Not yet implemented") + } + + override suspend fun getMyValuePicks(): Result { + TODO("Not yet implemented") + } + + override suspend fun updateMyValueTalks(valueTalks: List): Result { + val responses = valueTalks.map { valueTalk -> + MyValueTalkResponse( + id = valueTalk.id, + title = valueTalk.title, + category = valueTalk.category, + answer = valueTalk.answer, + summary = valueTalk.summary, + guides = valueTalk.guides, + ) + } + + return Result.success(GetMyValueTalksResponse(response = responses)) + } + + override suspend fun updateMyValuePicks(valuePicks: List): Result { + val responses = valuePicks.map { valuePick -> + MyValuePickResponse( + id = valuePick.id, + category = valuePick.category, + question = valuePick.question, + answerOptions = valuePick.answerOptions.map { + ValuePickAnswerResponse( + number = it.number, + content = it.content, + ) + }, + selectedAnswer = valuePick.selectedAnswer + ) + } + + return Result.success(GetMyValuePicksResponse(response = responses)) + } + + + override suspend fun updateMyProfileBasic( + description: String, + nickname: String, + birthDate: String, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, + snsActivityLevel: String, + imageUrl: String, + contacts: List + ): Result { + val response = GetMyProfileBasicResponse( + description = description, + nickname = nickname, + age = null, + birthDate = birthDate, + height = height, + weight = weight, + location = location, + job = job, + smokingStatus = smokingStatus, + snsActivityLevel = snsActivityLevel, + imageUrl = imageUrl, + contacts = contacts.map { contact -> + ContactResponse( + type = contact.type.name, + value = contact.content + ) + } + ) + + return Result.success(response) + } override suspend fun checkNickname(nickname: String): Result = Result.success(true) diff --git a/core/data/src/test/java/com/puzzle/data/fake/source/term/FakeLocalTermDataSource.kt b/core/data/src/test/java/com/puzzle/data/fake/source/term/FakeLocalTermDataSource.kt index 2bddce77..51b4d5b8 100644 --- a/core/data/src/test/java/com/puzzle/data/fake/source/term/FakeLocalTermDataSource.kt +++ b/core/data/src/test/java/com/puzzle/data/fake/source/term/FakeLocalTermDataSource.kt @@ -1,17 +1,23 @@ package com.puzzle.data.fake.source.term -import com.puzzle.database.model.terms.TermEntity -import com.puzzle.database.source.term.LocalTermDataSource +import com.puzzle.datastore.datasource.term.LocalTermDataSource +import com.puzzle.domain.model.terms.Term +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class FakeLocalTermDataSource : LocalTermDataSource { - private var terms = listOf() + private var storedTerms: List? = emptyList() - override suspend fun retrieveTerms(): List { - return terms + override val terms: Flow> = flow { + storedTerms?.let { emit(it) } + ?: throw NoSuchElementException("No value present in DataStore") } - override suspend fun replaceTerms(terms: List): Result { - this.terms = terms - return Result.success(Unit) + override suspend fun setTerms(terms: List) { + storedTerms = terms + } + + override suspend fun clearTerms() { + storedTerms = emptyList() } } diff --git a/core/data/src/test/java/com/puzzle/data/repository/AuthRepositoryImplTest.kt b/core/data/src/test/java/com/puzzle/data/repository/AuthRepositoryImplTest.kt index 6ebe8842..b5f3441e 100644 --- a/core/data/src/test/java/com/puzzle/data/repository/AuthRepositoryImplTest.kt +++ b/core/data/src/test/java/com/puzzle/data/repository/AuthRepositoryImplTest.kt @@ -32,7 +32,7 @@ class AuthRepositoryImplTest { @Test fun `유저가 회원가입에 성공했을 경우 토큰과 유저 상태를 저장한다`() = runTest { // when - val result = authRepository.loginOauth(OAuthProvider.KAKAO, "OAuthClientToken") + authRepository.loginOauth(OAuthProvider.KAKAO, "OAuthClientToken") // then assertTrue(localTokenDataSource.accessToken.first().isNotEmpty()) @@ -43,7 +43,7 @@ class AuthRepositoryImplTest { @Test fun `유저가 휴대폰 인증에 성공했을 경우 토큰과 유저 상태를 저장한다`() = runTest { // when - val result = authRepository.verifyAuthCode("01012341234", "authCode") + authRepository.verifyAuthCode("01012341234", "authCode") // then assertTrue(localTokenDataSource.accessToken.first().isNotEmpty()) 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 index 5edfc821..a82fe536 100644 --- a/core/data/src/test/java/com/puzzle/data/repository/MatchingRepositoryImplTest.kt +++ b/core/data/src/test/java/com/puzzle/data/repository/MatchingRepositoryImplTest.kt @@ -35,54 +35,66 @@ class MatchingRepositoryImplTest { @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" - ) + // given + val expectedOpponentProfile = dummyOpponentProfile + setOpponentProfile(expectedOpponentProfile) + + // When + matchingRepository.loadOpponentProfile().getOrThrow() + // Then + val storedProfile = fakeLocalMatchingDataSource.opponentProfile.first() + assertEquals(expectedOpponentProfile, storedProfile) + } + + private val dummyOpponentProfile = 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" + ) + + private fun setOpponentProfile(opponentProfile: OpponentProfile) { 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 + description = opponentProfile.description, + nickname = opponentProfile.nickname, + age = opponentProfile.age, + birthYear = opponentProfile.birthYear, + height = opponentProfile.height, + weight = opponentProfile.weight, + location = opponentProfile.location, + job = opponentProfile.job, + smokingStatus = opponentProfile.smokingStatus ), GetOpponentValuePicksResponse( matchId = 1, description = null, nickname = null, - valuePicks = expectedOpponentProfile.valuePicks.map { + valuePicks = opponentProfile.valuePicks.map { OpponentValuePickResponse( category = it.category, question = it.question, @@ -96,7 +108,7 @@ class MatchingRepositoryImplTest { matchId = 1, description = null, nickname = null, - valueTalks = expectedOpponentProfile.valueTalks.map { + valueTalks = opponentProfile.valueTalks.map { OpponentValueTalkResponse( category = it.category, summary = it.summary, @@ -104,14 +116,7 @@ class MatchingRepositoryImplTest { ) } ), - GetOpponentProfileImageResponse(imageUrl = expectedOpponentProfile.imageUrl) + GetOpponentProfileImageResponse(imageUrl = opponentProfile.imageUrl) ) - - // When - matchingRepository.loadOpponentProfile().getOrThrow() - - // Then - val storedProfile = fakeLocalMatchingDataSource.opponentProfile.first() - assertEquals(expectedOpponentProfile, storedProfile) } } diff --git a/core/data/src/test/java/com/puzzle/data/repository/ProfileRepositoryImplTest.kt b/core/data/src/test/java/com/puzzle/data/repository/ProfileRepositoryImplTest.kt index 4e257ffc..87cc726c 100644 --- a/core/data/src/test/java/com/puzzle/data/repository/ProfileRepositoryImplTest.kt +++ b/core/data/src/test/java/com/puzzle/data/repository/ProfileRepositoryImplTest.kt @@ -5,7 +5,11 @@ import com.puzzle.data.fake.source.profile.FakeProfileDataSource import com.puzzle.data.fake.source.token.FakeLocalTokenDataSource import com.puzzle.data.fake.source.user.FakeLocalUserDataSource import com.puzzle.data.spy.image.SpyImageResizer -import com.puzzle.network.model.matching.ValueTalkResponse +import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.ContactType +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.network.model.profile.ValueTalkResponse import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -61,13 +65,13 @@ class ProfileRepositoryImplTest { profileRepository.loadValueTalkQuestions() // then - val storedTalks = localProfileDataSource.retrieveValueTalkQuestions() + val storedTalks = localProfileDataSource.valueTalkQuestions.first() assertTrue(storedTalks.all { it.id != null }) assertTrue(storedTalks.size == 1) } @Test - fun `갱신한 가치관Talk 데이터는 로컬 데이터베이스에 저장한다`() = runTest { + fun `갱신한 가치관Talk 데이터는 로컬에 저장한다`() = runTest { // given val validValueTalks = listOf( ValueTalkResponse( @@ -89,14 +93,13 @@ class ProfileRepositoryImplTest { profileRepository.loadValueTalkQuestions() // then - val storedTalks = localProfileDataSource.retrieveValueTalkQuestions() - println(storedTalks.toString()) + val storedTalks = localProfileDataSource.valueTalkQuestions.first() assertTrue(storedTalks.size == validValueTalks.size) - assertTrue( - storedTalks.all { entity -> - validValueTalks.any { talk -> talk.id == entity.id && talk.title == entity.title } - } - ) + assertTrue(storedTalks.zip(validValueTalks).all { (stored, response) -> + stored.id == response.id && + stored.title == response.title && + stored.category == response.category + }) } @Test @@ -146,4 +149,80 @@ class ProfileRepositoryImplTest { // then assertEquals(1, imageResizer.resizeImageCallCount) } + + @Test + fun `가치관Talk 업데이트 시 로컬에 저장된다`() = runTest { + // given + val updatedValueTalks = listOf( + MyValueTalk( + id = 1, + category = "음주", + title = "술자리에 대한 대화", + answer = "가끔 즐깁니다.", + summary = "술자리 즐기기", + guides = emptyList(), + ) + ) + + // when + val result = profileRepository.updateMyValueTalks(updatedValueTalks) + + // then + val storedValueTalks = localProfileDataSource.myValueTalks.first() + assertEquals(updatedValueTalks, storedValueTalks) + assertEquals(updatedValueTalks, result.getOrNull()) + } + + @Test + fun `가치관Pick 업데이트 시 로컬에 저장된다`() = runTest { + // given + val updatedValuePicks = listOf( + MyValuePick( + id = 1, + category = "취미", + question = "주말에 주로 무엇을 하나요?", + answerOptions = listOf(), + selectedAnswer = 1 + ) + ) + + // when + val result = profileRepository.updateMyValuePicks(updatedValuePicks) + + // then + val storedValuePicks = localProfileDataSource.myValuePicks.first() + assertEquals(updatedValuePicks, storedValuePicks) + assertEquals(updatedValuePicks, result.getOrNull()) + } + + @Test + fun `프로필 기본 정보 업데이트 시 로컬에 저장된다`() = runTest { + // given + val contacts = listOf( + Contact(ContactType.KAKAO_TALK_ID, "user123"), + Contact(ContactType.PHONE_NUMBER, "010-1234-5678") + ) + + // when + val result = profileRepository.updateMyProfileBasic( + description = "새로운 자기소개", + nickname = "업데이트된닉네임", + birthDate = "1995-01-01", + height = 180, + weight = 75, + location = "서울 강남구", + job = "소프트웨어 엔지니어", + smokingStatus = "비흡연", + snsActivityLevel = "보통", + imageUrl = "updated_profile_image.jpg", + contacts = contacts + ) + + // then + val storedProfileBasic = localProfileDataSource.myProfileBasic.first() + + // 반환된 결과와 로컬에 저장된 프로필이 일치하는지 확인 + val updatedProfileBasic = result.getOrNull() + assertEquals(updatedProfileBasic, storedProfileBasic) + } } diff --git a/core/data/src/test/java/com/puzzle/data/repository/TermsRepositoryImplTest.kt b/core/data/src/test/java/com/puzzle/data/repository/TermsRepositoryImplTest.kt index 1a45c07e..5172085b 100644 --- a/core/data/src/test/java/com/puzzle/data/repository/TermsRepositoryImplTest.kt +++ b/core/data/src/test/java/com/puzzle/data/repository/TermsRepositoryImplTest.kt @@ -4,6 +4,7 @@ import com.puzzle.data.fake.source.term.FakeLocalTermDataSource import com.puzzle.data.fake.source.term.FakeTermDataSource import com.puzzle.network.model.UNKNOWN_INT import com.puzzle.network.model.terms.TermResponse +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach @@ -46,9 +47,9 @@ class TermsRepositoryImplTest { termsRepository.loadTerms() // then - val storedTerms = localTermDataSource.retrieveTerms() + val storedTerms = localTermDataSource.terms.first() assertTrue(storedTerms.all { it.id != UNKNOWN_INT }) - assertTrue(storedTerms.size == 1) // 유효한 약관 하나만 저장되어야 함 + assertTrue(storedTerms.size == 1) } @Test @@ -76,13 +77,14 @@ class TermsRepositoryImplTest { termsRepository.loadTerms() // then - val storedTerms = localTermDataSource.retrieveTerms() + val storedTerms = localTermDataSource.terms.first() assertTrue(storedTerms.size == validTerms.size) assertTrue( - storedTerms.all { entity -> - validTerms.any { term -> - term.termId == entity.id && term.title == entity.title - } + storedTerms.zip(validTerms).all { (stored, response) -> + stored.id == response.termId && + stored.title == response.title && + stored.content == response.content && + stored.required == response.required } ) } diff --git a/core/database/.gitignore b/core/database/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/core/database/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts deleted file mode 100644 index 6a86ca5f..00000000 --- a/core/database/build.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id("piece.android.library") - id("piece.android.hilt") -} - -android { - namespace = "com.puzzle.database" -} - -dependencies { - implementation(projects.core.domain) - implementation(projects.core.common) - - implementation(libs.androidx.room.ktx) - ksp(libs.androidx.room.compiler) -} diff --git a/core/database/consumer-rules.pro b/core/database/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/core/database/proguard-rules.pro b/core/database/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/core/database/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/database/src/androidTest/java/com/puzzle/database/dao/MyValuePicksDaoTestQuestion.kt b/core/database/src/androidTest/java/com/puzzle/database/dao/MyValuePicksDaoTestQuestion.kt deleted file mode 100644 index dabc51bd..00000000 --- a/core/database/src/androidTest/java/com/puzzle/database/dao/MyValuePicksDaoTestQuestion.kt +++ /dev/null @@ -1,182 +0,0 @@ -package com.puzzle.database.dao - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.puzzle.database.PieceDatabase -import com.puzzle.database.model.matching.ValuePickAnswerEntity -import com.puzzle.database.model.matching.ValuePickEntity -import com.puzzle.database.model.matching.ValuePickQuestionEntity -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class MyValuePicksDaoTestQuestion { - private lateinit var valuePicksDao: ValuePicksDao - private lateinit var db: PieceDatabase - - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - db = Room.inMemoryDatabaseBuilder( - context = context, - klass = PieceDatabase::class.java - ).build() - valuePicksDao = db.valuePicksDao() - } - - @After - fun tearDown() { - db.close() - } - - @Test - fun 가치관Pick을_삽입하고_조회할_수_있다() = runTest { - // given - val valuePickQuestion = ValuePickQuestionEntity( - id = 1, - category = "음주", - question = "사귀는 사람과 함께 술을 마시는 것을 좋아하나요?" - ) - - val valuePickAnswers = listOf( - ValuePickAnswerEntity( - id = 1, - questionsId = 1, - number = 1, - content = "함께 술을 즐기고 싶어요" - ), - ValuePickAnswerEntity( - id = 2, - questionsId = 1, - number = 2, - content = "같이 술을 즐길 수 없어도 괜찮아요" - ) - ) - - val expected = ValuePickEntity( - valuePickQuestion = valuePickQuestion, - answers = valuePickAnswers - ) - - // when - valuePicksDao.insertValuePickQuestion(expected.valuePickQuestion) - valuePicksDao.insertValuePickAnswers(expected.answers) - val actual = valuePicksDao.getValuePicks() - - // then - assertEquals(listOf(expected), actual) - } - - @Test - fun 가치관Pick을_모두_삭제할_수_있다() = runTest { - // given - val valuePickQuestion = ValuePickQuestionEntity( - id = 1, - category = "음주", - question = "사귀는 사람과 함께 술을 마시는 것을 좋아하나요?" - ) - - val valuePickAnswers = listOf( - ValuePickAnswerEntity( - id = 1, - questionsId = 1, - number = 1, - content = "함께 술을 즐기고 싶어요" - ), - ValuePickAnswerEntity( - id = 2, - questionsId = 1, - number = 2, - content = "같이 술을 즐길 수 없어도 괜찮아요" - ) - ) - - val valuePick = ValuePickEntity( - valuePickQuestion = valuePickQuestion, - answers = valuePickAnswers - ) - - valuePicksDao.insertValuePickQuestion(valuePick.valuePickQuestion) - valuePicksDao.insertValuePickAnswers(valuePick.answers) - - // when - valuePicksDao.clearValuePicks() - val actual = valuePicksDao.getValuePicks() - - // then - val expected = emptyList() - assertEquals(expected, actual) - } - - @Test - fun 이전_가치관Pick을_삭제하고_새로운_가치관Pick을_삽입할_수_있다() = runTest { - // given - val oldValuePickQuestion = ValuePickQuestionEntity( - id = 1, - category = "이전 카테고리", - question = "이전 질문" - ) - - val oldValuePickAnswers = listOf( - ValuePickAnswerEntity( - id = 1, - questionsId = 1, - number = 1, - content = "이전 답변 1" - ), - ValuePickAnswerEntity( - id = 2, - questionsId = 1, - number = 2, - content = "이전 답변 2" - ) - ) - - val oldValuePick = ValuePickEntity( - valuePickQuestion = oldValuePickQuestion, - answers = oldValuePickAnswers - ) - - valuePicksDao.insertValuePickQuestion(oldValuePick.valuePickQuestion) - valuePicksDao.insertValuePickAnswers(oldValuePick.answers) - - // when - val newValuePickQuestion = ValuePickQuestionEntity( - id = 2, - category = "새로운 카테고리", - question = "새로운 질문" - ) - - val newValuePickAnswers = listOf( - ValuePickAnswerEntity( - id = 3, - questionsId = 2, - number = 1, - content = "새로운 답변 1" - ), - ValuePickAnswerEntity( - id = 4, - questionsId = 2, - number = 2, - content = "새로운 답변 2" - ) - ) - - val expected = ValuePickEntity( - valuePickQuestion = newValuePickQuestion, - answers = newValuePickAnswers - ) - - valuePicksDao.replaceValuePicks(expected) - val actual = valuePicksDao.getValuePicks() - - // then - assertEquals(expected, actual) - } -} diff --git a/core/database/src/androidTest/java/com/puzzle/database/dao/MyValueTalksDaoTestQuestion.kt b/core/database/src/androidTest/java/com/puzzle/database/dao/MyValueTalksDaoTestQuestion.kt deleted file mode 100644 index 7107d887..00000000 --- a/core/database/src/androidTest/java/com/puzzle/database/dao/MyValueTalksDaoTestQuestion.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.puzzle.database.dao - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.puzzle.database.PieceDatabase -import com.puzzle.database.model.matching.ValueTalkEntity -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class MyValueTalksDaoTestQuestion { - private lateinit var valueTalksDao: ValueTalksDao - private lateinit var db: PieceDatabase - - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - db = Room.inMemoryDatabaseBuilder( - context = context, - klass = PieceDatabase::class.java - ).build() - valueTalksDao = db.valueTalksDao() - } - - @After - fun tearDown() { - db.close() - } - - @Test - fun 가치관Talk을_삽입하고_조회할_수_있다() = runTest { - // given - val valueTalk = ValueTalkEntity( - id = 1, - category = "음주", - title = "술자리에 대한 생각", - helpMessages = listOf( - "술을 즐기는 방식이 다를 수 있어요", - "서로의 취향을 존중하는 게 중요해요" - ) - ) - - // when - valueTalksDao.insertValueTalks(valueTalk) - val actual = valueTalksDao.getValueTalks() - - // then - assertEquals(listOf(valueTalk), actual) - } - - @Test - fun 가치관Talk을_모두_삭제할_수_있다() = runTest { - // given - val valueTalk = ValueTalkEntity( - id = 1, - category = "음주", - title = "술자리에 대한 생각", - helpMessages = listOf( - "술을 즐기는 방식이 다를 수 있어요", - "서로의 취향을 존중하는 게 중요해요" - ) - ) - - valueTalksDao.insertValueTalks(valueTalk) - - // when - valueTalksDao.clearValueTalks() - val actual = valueTalksDao.getValueTalks() - - // then - val expected = emptyList() - assertEquals(expected, actual) - } - - @Test - fun 이전_가치관Talk을_삭제하고_새로운_가치관Talk을_삽입할_수_있다() = runTest { - // given - val oldValueTalk = ValueTalkEntity( - id = 1, - category = "이전 카테고리", - title = "이전 제목", - helpMessages = listOf("이전 메시지 1", "이전 메시지 2") - ) - - valueTalksDao.insertValueTalks(oldValueTalk) - - // when - val newValueTalk = listOf( - ValueTalkEntity( - id = 2, - category = "새로운 카테고리", - title = "새로운 제목", - helpMessages = listOf("새로운 메시지 1", "새로운 메시지 2") - ), - ValueTalkEntity( - id = 3, - category = "새로운 카테고리", - title = "새로운 제목", - helpMessages = listOf("새로운 메시지 3", "새로운 메시지 4") - ) - ) - - valueTalksDao.replaceValueTalks(*newValueTalk.toTypedArray()) - val actual = valueTalksDao.getValueTalks() - - // then - assertEquals(newValueTalk, actual) - } -} diff --git a/core/database/src/androidTest/java/com/puzzle/database/dao/TermsDaoTest.kt b/core/database/src/androidTest/java/com/puzzle/database/dao/TermsDaoTest.kt deleted file mode 100644 index ebbd886f..00000000 --- a/core/database/src/androidTest/java/com/puzzle/database/dao/TermsDaoTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.puzzle.database.dao - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.puzzle.common.parseDateTime -import com.puzzle.database.PieceDatabase -import com.puzzle.database.model.terms.TermEntity -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class TermsDaoTest { - private lateinit var termsDao: TermsDao - private lateinit var db: PieceDatabase - - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - db = Room.inMemoryDatabaseBuilder( - context = context, - klass = PieceDatabase::class.java - ).build() - termsDao = db.termsDao() - } - - @After - fun tearDown() { - db.close() - } - - @Test - fun 약관을_삽입하고_조회할_수_있다() = runTest { - // given - val expected = listOf( - TermEntity(1, "이용약관", "내용1", true, "2024-06-01T00:00:00".parseDateTime()), - TermEntity(2, "개인정보처리방침", "내용2", false, "2024-06-01T00:00:00".parseDateTime()) - ) - - // when - termsDao.insertTerms(*expected.toTypedArray()) - val actual = termsDao.getTerms() - - // then - assertEquals(expected, actual) - } - - @Test - fun 약관을_모두_삭제할_수_있다() = runTest { - // given - val terms = listOf( - TermEntity(1, "이용약관", "내용1", true, "2024-06-01T00:00:00".parseDateTime()), - TermEntity(2, "개인정보처리방침", "내용2", false, "2024-06-01T00:00:00".parseDateTime()) - ) - termsDao.insertTerms(*terms.toTypedArray()) - - // when - termsDao.clearTerms() - val actual = termsDao.getTerms() - - // then - val expected = emptyList() - assertEquals(expected, actual) - } - - @Test - fun 이전_약관을_삭제하고_새로운_약관을_삽입할_수_있다() = runTest { - // given - val oldTerms = listOf( - TermEntity(1, "이전 약관", "이전 내용", true, "2024-06-01T00:00:00".parseDateTime()) - ) - termsDao.insertTerms(*oldTerms.toTypedArray()) - - // when - val expected = listOf( - TermEntity(2, "새로운 약관", "새로운 내용", false, "2024-06-01T00:00:00".parseDateTime()) - ) - termsDao.replaceTerms(*expected.toTypedArray()) - val actual = termsDao.getTerms() - - // then - assertEquals(expected, actual) - } -} diff --git a/core/database/src/main/AndroidManifest.xml b/core/database/src/main/AndroidManifest.xml deleted file mode 100644 index 44008a43..00000000 --- a/core/database/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/core/database/src/main/java/com/puzzle/database/PieceDatabase.kt b/core/database/src/main/java/com/puzzle/database/PieceDatabase.kt deleted file mode 100644 index 83b3894d..00000000 --- a/core/database/src/main/java/com/puzzle/database/PieceDatabase.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.puzzle.database - -import androidx.room.Database -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import com.puzzle.database.converter.PieceConverters -import com.puzzle.database.dao.TermsDao -import com.puzzle.database.dao.ValuePicksDao -import com.puzzle.database.dao.ValueTalksDao -import com.puzzle.database.model.matching.ValuePickAnswerEntity -import com.puzzle.database.model.matching.ValuePickQuestionEntity -import com.puzzle.database.model.matching.ValueTalkEntity -import com.puzzle.database.model.terms.TermEntity - -@Database( - entities = [ - TermEntity::class, - ValuePickQuestionEntity::class, - ValuePickAnswerEntity::class, - ValueTalkEntity::class, - ], - version = 1, -) -@TypeConverters(PieceConverters::class) -internal abstract class PieceDatabase : RoomDatabase() { - abstract fun termsDao(): TermsDao - abstract fun valuePicksDao(): ValuePicksDao - abstract fun valueTalksDao(): ValueTalksDao - - companion object { - internal const val NAME = "piece-database" - } -} diff --git a/core/database/src/main/java/com/puzzle/database/converter/PieceConverters.kt b/core/database/src/main/java/com/puzzle/database/converter/PieceConverters.kt deleted file mode 100644 index c557e8b7..00000000 --- a/core/database/src/main/java/com/puzzle/database/converter/PieceConverters.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.puzzle.database.converter - -import androidx.room.TypeConverter -import com.puzzle.common.parseDateTime -import java.time.LocalDateTime - -class PieceConverters { - @TypeConverter - fun fromLocalDate(date: LocalDateTime?): String? { - return date?.toString() - } - - @TypeConverter - fun toLocalDate(dateString: String?): LocalDateTime? { - return dateString?.parseDateTime() - } - - @TypeConverter - fun fromStringList(value: List?): String? { - return value?.joinToString(",") - } - - @TypeConverter - fun toStringList(value: String?): List? { - return value?.split(",")?.filter { it.isNotEmpty() } - } -} diff --git a/core/database/src/main/java/com/puzzle/database/dao/TermsDao.kt b/core/database/src/main/java/com/puzzle/database/dao/TermsDao.kt deleted file mode 100644 index 42060f62..00000000 --- a/core/database/src/main/java/com/puzzle/database/dao/TermsDao.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.puzzle.database.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.puzzle.database.model.terms.TermEntity - -@Dao -interface TermsDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertTerms(vararg terms: TermEntity) - - @Query(value = "SELECT * FROM term") - suspend fun getTerms(): List - - @Query(value = "DELETE FROM term") - suspend fun clearTerms() - - @Transaction - suspend fun replaceTerms(vararg terms: TermEntity) { - clearTerms() - insertTerms(*terms) - } -} diff --git a/core/database/src/main/java/com/puzzle/database/dao/ValuePicksDao.kt b/core/database/src/main/java/com/puzzle/database/dao/ValuePicksDao.kt deleted file mode 100644 index c8fd1ded..00000000 --- a/core/database/src/main/java/com/puzzle/database/dao/ValuePicksDao.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.puzzle.database.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.puzzle.database.model.matching.ValuePickAnswerEntity -import com.puzzle.database.model.matching.ValuePickEntity -import com.puzzle.database.model.matching.ValuePickQuestionEntity - -@Dao -interface ValuePicksDao { - @Transaction - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertValuePickQuestion(question: ValuePickQuestionEntity) - - @Transaction - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertValuePickAnswers(answers: List) - - @Transaction - @Query("SELECT * FROM value_pick_question") - suspend fun getValuePicks(): List - - @Transaction - @Query("DELETE FROM value_pick_question") - suspend fun clearValuePicks() - - @Transaction - suspend fun replaceValuePicks(vararg valuePicks: ValuePickEntity) { - clearValuePicks() - valuePicks.forEach { valuePick -> - insertValuePickQuestion(valuePick.valuePickQuestion) - insertValuePickAnswers(valuePick.answers) - } - } -} diff --git a/core/database/src/main/java/com/puzzle/database/dao/ValueTalksDao.kt b/core/database/src/main/java/com/puzzle/database/dao/ValueTalksDao.kt deleted file mode 100644 index f9c588f5..00000000 --- a/core/database/src/main/java/com/puzzle/database/dao/ValueTalksDao.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.puzzle.database.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.puzzle.database.model.matching.ValueTalkEntity - -@Dao -interface ValueTalksDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertValueTalks(vararg valueTalks: ValueTalkEntity) - - @Query(value = "SELECT * FROM value_talk") - suspend fun getValueTalks(): List - - @Query(value = "DELETE FROM value_talk") - suspend fun clearValueTalks() - - @Transaction - suspend fun replaceValueTalks(vararg valueTalks: ValueTalkEntity) { - clearValueTalks() - insertValueTalks(*valueTalks) - } -} diff --git a/core/database/src/main/java/com/puzzle/database/di/DaosModule.kt b/core/database/src/main/java/com/puzzle/database/di/DaosModule.kt deleted file mode 100644 index 05b7e30f..00000000 --- a/core/database/src/main/java/com/puzzle/database/di/DaosModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.puzzle.database.di - -import com.puzzle.database.PieceDatabase -import com.puzzle.database.dao.TermsDao -import com.puzzle.database.dao.ValuePicksDao -import com.puzzle.database.dao.ValueTalksDao -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal object DaosModule { - @Provides - fun providesTermsDao( - database: PieceDatabase, - ): TermsDao = database.termsDao() - - @Provides - fun providesValuePicksDao( - database: PieceDatabase, - ): ValuePicksDao = database.valuePicksDao() - - @Provides - fun providesValueTalksDao( - database: PieceDatabase, - ): ValueTalksDao = database.valueTalksDao() -} diff --git a/core/database/src/main/java/com/puzzle/database/di/DatabaseModule.kt b/core/database/src/main/java/com/puzzle/database/di/DatabaseModule.kt deleted file mode 100644 index 39e6f68a..00000000 --- a/core/database/src/main/java/com/puzzle/database/di/DatabaseModule.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.puzzle.database.di - -import android.content.Context -import androidx.room.Room -import com.puzzle.database.PieceDatabase -import com.puzzle.database.source.profile.LocalProfileDataSource -import com.puzzle.database.source.profile.LocalProfileDataSourceImpl -import com.puzzle.database.source.term.LocalTermDataSource -import com.puzzle.database.source.term.LocalTermDataSourceImpl -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object DatabaseProvidesModule { - @Provides - @Singleton - fun providesPieceDatabase( - @ApplicationContext context: Context, - ): PieceDatabase = Room.databaseBuilder( - context, - PieceDatabase::class.java, - PieceDatabase.NAME, - ).build() -} - -@Module -@InstallIn(SingletonComponent::class) -abstract class DatabaseBindsModule { - - @Binds - @Singleton - abstract fun bindsLocalProfileDataSource( - localProfileDataSourceImpl: LocalProfileDataSourceImpl - ): LocalProfileDataSource - - @Binds - @Singleton - abstract fun bindsLocalTermDataSource( - localTermDataSourceImpl: LocalTermDataSourceImpl - ): LocalTermDataSource -} diff --git a/core/database/src/main/java/com/puzzle/database/model/matching/ValuePickEntity.kt b/core/database/src/main/java/com/puzzle/database/model/matching/ValuePickEntity.kt deleted file mode 100644 index 02de9670..00000000 --- a/core/database/src/main/java/com/puzzle/database/model/matching/ValuePickEntity.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.puzzle.database.model.matching - -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index -import androidx.room.PrimaryKey -import androidx.room.Relation -import com.puzzle.domain.model.profile.AnswerOption -import com.puzzle.domain.model.profile.ValuePickQuestion - -data class ValuePickEntity( - @Embedded val valuePickQuestion: ValuePickQuestionEntity, - @Relation( - parentColumn = "id", - entityColumn = "questionsId", - entity = ValuePickAnswerEntity::class, - ) - val answers: List, -) { - fun toDomain() = ValuePickQuestion( - id = valuePickQuestion.id, - category = valuePickQuestion.category, - question = valuePickQuestion.question, - answerOptions = answers.map { - AnswerOption( - number = it.number, - content = it.content, - ) - }, - ) -} - -@Entity(tableName = "value_pick_question") -data class ValuePickQuestionEntity( - @PrimaryKey val id: Int, - val category: String, - val question: String, -) - -@Entity( - tableName = "value_pick_answer", - foreignKeys = [ - ForeignKey( - entity = ValuePickQuestionEntity::class, - parentColumns = ["id"], - childColumns = ["questionsId"], - onDelete = ForeignKey.CASCADE - ) - ], - indices = [Index("questionsId")] -) -data class ValuePickAnswerEntity( - @PrimaryKey(autoGenerate = true) val id: Int? = null, - val questionsId: Int, - val number: Int, - val content: String, -) diff --git a/core/database/src/main/java/com/puzzle/database/model/matching/ValueTalkEntity.kt b/core/database/src/main/java/com/puzzle/database/model/matching/ValueTalkEntity.kt deleted file mode 100644 index bc4515af..00000000 --- a/core/database/src/main/java/com/puzzle/database/model/matching/ValueTalkEntity.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.puzzle.database.model.matching - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.puzzle.domain.model.profile.ValueTalkQuestion - -@Entity(tableName = "value_talk") -data class ValueTalkEntity( - @PrimaryKey val id: Int, - val category: String, - val title: String, - val helpMessages: List, -) { - fun toDomain() = ValueTalkQuestion( - id = id, - category = category, - title = title, - guides = helpMessages, - ) -} diff --git a/core/database/src/main/java/com/puzzle/database/model/terms/TermEntity.kt b/core/database/src/main/java/com/puzzle/database/model/terms/TermEntity.kt deleted file mode 100644 index a6897ec5..00000000 --- a/core/database/src/main/java/com/puzzle/database/model/terms/TermEntity.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.puzzle.database.model.terms - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.puzzle.domain.model.terms.Term -import java.time.LocalDateTime - -@Entity(tableName = "term") -data class TermEntity( - @PrimaryKey val id: Int, - val title: String, - val content: String, - val required: Boolean, - @ColumnInfo(name = "start_date") val startDate: LocalDateTime, -) { - fun toDomain() = Term( - id = id, - title = title, - content = content, - required = required, - startDate = startDate, - ) -} diff --git a/core/database/src/main/java/com/puzzle/database/source/profile/LocalProfileDataSource.kt b/core/database/src/main/java/com/puzzle/database/source/profile/LocalProfileDataSource.kt deleted file mode 100644 index 3a10838d..00000000 --- a/core/database/src/main/java/com/puzzle/database/source/profile/LocalProfileDataSource.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.puzzle.database.source.profile - -import com.puzzle.database.model.matching.ValuePickEntity -import com.puzzle.database.model.matching.ValueTalkEntity - -interface LocalProfileDataSource { - suspend fun retrieveValuePickQuestions(): List - suspend fun replaceValuePickQuestions(valuePicks: List): Result - - suspend fun retrieveValueTalkQuestions(): List - suspend fun replaceValueTalkQuestions(valueTalks: List): Result -} diff --git a/core/database/src/main/java/com/puzzle/database/source/profile/LocalProfileDataSourceImpl.kt b/core/database/src/main/java/com/puzzle/database/source/profile/LocalProfileDataSourceImpl.kt deleted file mode 100644 index ff8214e9..00000000 --- a/core/database/src/main/java/com/puzzle/database/source/profile/LocalProfileDataSourceImpl.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.puzzle.database.source.profile - -import com.puzzle.common.suspendRunCatching -import com.puzzle.database.dao.ValuePicksDao -import com.puzzle.database.dao.ValueTalksDao -import com.puzzle.database.model.matching.ValuePickEntity -import com.puzzle.database.model.matching.ValueTalkEntity -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class LocalProfileDataSourceImpl @Inject constructor( - private val valuePicksDao: ValuePicksDao, - private val valueTalksDao: ValueTalksDao, -) : LocalProfileDataSource { - override suspend fun retrieveValuePickQuestions() = valuePicksDao.getValuePicks() - override suspend fun replaceValuePickQuestions(valuePicks: List) = - suspendRunCatching { - valuePicksDao.replaceValuePicks(*valuePicks.toTypedArray()) - } - - override suspend fun retrieveValueTalkQuestions() = valueTalksDao.getValueTalks() - override suspend fun replaceValueTalkQuestions(valueTalks: List) = - suspendRunCatching { - valueTalksDao.replaceValueTalks(*valueTalks.toTypedArray()) - } -} diff --git a/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSource.kt b/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSource.kt deleted file mode 100644 index e0637375..00000000 --- a/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSource.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.puzzle.database.source.term - -import com.puzzle.database.model.terms.TermEntity - -interface LocalTermDataSource { - suspend fun retrieveTerms(): List - suspend fun replaceTerms(terms: List): Result -} diff --git a/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSourceImpl.kt b/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSourceImpl.kt deleted file mode 100644 index 8744f092..00000000 --- a/core/database/src/main/java/com/puzzle/database/source/term/LocalTermDataSourceImpl.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.puzzle.database.source.term - -import com.puzzle.common.suspendRunCatching -import com.puzzle.database.dao.TermsDao -import com.puzzle.database.model.terms.TermEntity -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class LocalTermDataSourceImpl @Inject constructor( - private val termsDao: TermsDao, -) : LocalTermDataSource { - override suspend fun retrieveTerms() = termsDao.getTerms() - override suspend fun replaceTerms(terms: List) = suspendRunCatching { - termsDao.replaceTerms(*terms.toTypedArray()) - } -} 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 index ef7527b4..aa8e9d53 100644 --- 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 @@ -15,8 +15,8 @@ import javax.inject.Named class LocalMatchingDataSourceImpl @Inject constructor( @Named("matching") private val dataStore: DataStore, + private val gson: Gson, ) : LocalMatchingDataSource { - private val gson = Gson() override val opponentProfile: Flow = dataStore.getValue(OPPONENT_PROFILE, "") .map { gson.fromJson(it, OpponentProfile::class.java) } diff --git a/core/datastore/src/main/java/com/puzzle/datastore/datasource/profile/LocalProfileDataSource.kt b/core/datastore/src/main/java/com/puzzle/datastore/datasource/profile/LocalProfileDataSource.kt new file mode 100644 index 00000000..e735fd88 --- /dev/null +++ b/core/datastore/src/main/java/com/puzzle/datastore/datasource/profile/LocalProfileDataSource.kt @@ -0,0 +1,27 @@ +package com.puzzle.datastore.datasource.profile + +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.domain.model.profile.ValuePickQuestion +import com.puzzle.domain.model.profile.ValueTalkQuestion +import kotlinx.coroutines.flow.Flow + +interface LocalProfileDataSource { + val valuePickQuestions: Flow> + suspend fun setValuePickQuestions(valuePicks: List) + + val valueTalkQuestions: Flow> + suspend fun setValueTalkQuestions(valueTalks: List) + + val myProfileBasic: Flow + suspend fun setMyProfileBasic(profileBasic: MyProfileBasic) + + val myValuePicks: Flow> + suspend fun setMyValuePicks(valuePicks: List) + + val myValueTalks: Flow> + suspend fun setMyValueTalks(valueTalks: List) + + suspend fun clearMyProfile() +} diff --git a/core/datastore/src/main/java/com/puzzle/datastore/datasource/profile/LocalProfileDataSourceImpl.kt b/core/datastore/src/main/java/com/puzzle/datastore/datasource/profile/LocalProfileDataSourceImpl.kt new file mode 100644 index 00000000..cc74de97 --- /dev/null +++ b/core/datastore/src/main/java/com/puzzle/datastore/datasource/profile/LocalProfileDataSourceImpl.kt @@ -0,0 +1,87 @@ +package com.puzzle.datastore.datasource.profile + +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.google.gson.reflect.TypeToken +import com.puzzle.datastore.util.getValue +import com.puzzle.datastore.util.setValue +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.domain.model.profile.ValuePickQuestion +import com.puzzle.domain.model.profile.ValueTalkQuestion +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class LocalProfileDataSourceImpl @Inject constructor( + @Named("profile") private val dataStore: DataStore, + private val gson: Gson, +) : LocalProfileDataSource { + override val valuePickQuestions: Flow> = + dataStore.getValue(VALUE_PICK_QUESTIONS, "") + .map { jsonString -> + gson.fromJson(jsonString, object : TypeToken>() {}.type) + } + + override suspend fun setValuePickQuestions(valuePicks: List) { + val jsonString = gson.toJson(valuePicks) + dataStore.setValue(VALUE_PICK_QUESTIONS, jsonString) + } + + override val valueTalkQuestions: Flow> = + dataStore.getValue(VALUE_TALK_QUESTIONS, "") + .map { gson.fromJson(it, object : TypeToken>() {}.type) } + + override suspend fun setValueTalkQuestions(valueTalks: List) { + val jsonString = gson.toJson(valueTalks) + dataStore.setValue(VALUE_TALK_QUESTIONS, jsonString) + } + + override val myProfileBasic: Flow = dataStore.getValue(MY_PROFILE_BASIC, "") + .map { gson.fromJson(it, MyProfileBasic::class.java) } + + override suspend fun setMyProfileBasic(profileBasic: MyProfileBasic) { + val jsonString = gson.toJson(profileBasic) + dataStore.setValue(MY_PROFILE_BASIC, jsonString) + } + + override val myValuePicks: Flow> = dataStore.getValue(MY_VALUE_PICKS, "") + .map { gson.fromJson(it, object : TypeToken>() {}.type) } + + + override suspend fun setMyValuePicks(valuePicks: List) { + val jsonString = gson.toJson(valuePicks) + dataStore.setValue(MY_VALUE_PICKS, jsonString) + } + + override val myValueTalks: Flow> = dataStore.getValue(MY_VALUE_TALKS, "") + .map { gson.fromJson(it, object : TypeToken>() {}.type) } + + override suspend fun setMyValueTalks(valueTalks: List) { + val jsonString = gson.toJson(valueTalks) + dataStore.setValue(MY_VALUE_TALKS, jsonString) + } + + override suspend fun clearMyProfile() { + dataStore.edit { preferences -> + preferences.remove(MY_PROFILE_BASIC) + preferences.remove(MY_VALUE_TALKS) + preferences.remove(MY_VALUE_PICKS) + } + } + + companion object { + private val VALUE_PICK_QUESTIONS = stringPreferencesKey("VALUE_PICK_QUESTIONS") + private val VALUE_TALK_QUESTIONS = stringPreferencesKey("VALUE_TALK_QUESTIONS") + private val MY_PROFILE_BASIC = stringPreferencesKey("MY_PROFILE_BASIC") + private val MY_VALUE_PICKS = stringPreferencesKey("MY_VALUE_PICKS") + private val MY_VALUE_TALKS = stringPreferencesKey("MY_VALUE_TALKS") + } +} diff --git a/core/datastore/src/main/java/com/puzzle/datastore/datasource/term/LocalTermDataSource.kt b/core/datastore/src/main/java/com/puzzle/datastore/datasource/term/LocalTermDataSource.kt new file mode 100644 index 00000000..b64f2b23 --- /dev/null +++ b/core/datastore/src/main/java/com/puzzle/datastore/datasource/term/LocalTermDataSource.kt @@ -0,0 +1,10 @@ +package com.puzzle.datastore.datasource.term + +import com.puzzle.domain.model.terms.Term +import kotlinx.coroutines.flow.Flow + +interface LocalTermDataSource { + val terms: Flow> + suspend fun setTerms(terms: List) + suspend fun clearTerms() +} diff --git a/core/datastore/src/main/java/com/puzzle/datastore/datasource/term/LocalTermDataSourceImpl.kt b/core/datastore/src/main/java/com/puzzle/datastore/datasource/term/LocalTermDataSourceImpl.kt new file mode 100644 index 00000000..8981000c --- /dev/null +++ b/core/datastore/src/main/java/com/puzzle/datastore/datasource/term/LocalTermDataSourceImpl.kt @@ -0,0 +1,40 @@ +package com.puzzle.datastore.datasource.term + +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.google.gson.reflect.TypeToken +import com.puzzle.datastore.util.getValue +import com.puzzle.datastore.util.setValue +import com.puzzle.domain.model.terms.Term +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class LocalTermDataSourceImpl @Inject constructor( + @Named("term") private val dataStore: DataStore, + private val gson: Gson, +) : LocalTermDataSource { + override val terms: Flow> = dataStore.getValue(TERM, "") + .map { jsonString -> + gson.fromJson(jsonString, object : TypeToken>() {}.type) + } + + override suspend fun setTerms(terms: List) { + val jsonString = gson.toJson(terms) + dataStore.setValue(TERM, jsonString) + } + + override suspend fun clearTerms() { + dataStore.edit { preferences -> preferences.remove(TERM) } + } + + companion object { + private val TERM = stringPreferencesKey("TERM") + } +} 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 bf76b4ad..7576f586 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,11 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import com.puzzle.datastore.datasource.matching.LocalMatchingDataSource import com.puzzle.datastore.datasource.matching.LocalMatchingDataSourceImpl import com.puzzle.datastore.datasource.token.LocalTokenDataSource @@ -16,6 +21,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import java.time.LocalDateTime import javax.inject.Named import javax.inject.Singleton @@ -31,6 +37,30 @@ object DataStoreProvidesModule { private const val MATCHING_DATASTORE_NAME = "MATCHING_PREFERENCES" private val Context.matchingDataStore by preferencesDataStore(name = MATCHING_DATASTORE_NAME) + private const val PROFILE_DATASTORE_NAME = "PROFILE_PREFERENCES" + private val Context.profileDataStore by preferencesDataStore(name = PROFILE_DATASTORE_NAME) + + private const val TERM_DATASTORE_NAME = "TERM_PREFERENCES" + private val Context.termDataStore by preferencesDataStore(name = TERM_DATASTORE_NAME) + + @Provides + @Singleton + fun provideGson(): Gson = GsonBuilder() + .registerTypeAdapter(LocalDateTime::class.java, object : TypeAdapter() { + override fun write(out: JsonWriter, value: LocalDateTime?) { + value?.let { out.value(it.toString()) } ?: out.nullValue() + } + + override fun read(input: JsonReader): LocalDateTime? { + return try { + LocalDateTime.parse(input.nextString()) + } catch (e: Exception) { + null + } + } + }) + .create() + @Provides @Singleton @Named("token") @@ -51,6 +81,20 @@ object DataStoreProvidesModule { fun provideMatchingDataStore( @ApplicationContext context: Context ): DataStore = context.matchingDataStore + + @Provides + @Singleton + @Named("profile") + fun provideProfileDataStore( + @ApplicationContext context: Context + ): DataStore = context.profileDataStore + + @Provides + @Singleton + @Named("term") + fun provideTermDataStore( + @ApplicationContext context: Context + ): DataStore = context.termDataStore } @Module @@ -74,4 +118,16 @@ abstract class DatastoreBindsModule { abstract fun bindsLocalMatchingDataSource( localMatchingDataSourceImpl: LocalMatchingDataSourceImpl, ): LocalMatchingDataSource + + @Binds + @Singleton + abstract fun bindsLocalProfileDataSource( + localProfileDataSourceImpl: com.puzzle.datastore.datasource.profile.LocalProfileDataSourceImpl + ): com.puzzle.datastore.datasource.profile.LocalProfileDataSource + + @Binds + @Singleton + abstract fun bindsLocalTermDataSource( + localTermDataSourceImpl: com.puzzle.datastore.datasource.term.LocalTermDataSourceImpl + ): com.puzzle.datastore.datasource.term.LocalTermDataSource } diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Button.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Button.kt index b3519359..9886d4b0 100644 --- a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Button.kt +++ b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Button.kt @@ -209,7 +209,7 @@ fun PieceLoginButton( } @Composable -fun PieceRoundingButton( +fun PieceRoundingSolidButton( label: String, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -238,6 +238,35 @@ fun PieceRoundingButton( } } +@Composable +fun PieceRoundingOutlinedButton( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + Button( + onClick = onClick, + enabled = enabled, + shape = RoundedCornerShape(46.dp), + border = BorderStroke(width = 1.dp, color = PieceTheme.colors.primaryDefault), + colors = ButtonDefaults.buttonColors( + containerColor = PieceTheme.colors.white, + contentColor = PieceTheme.colors.primaryDefault, + disabledContainerColor = PieceTheme.colors.light1, + disabledContentColor = PieceTheme.colors.primaryDefault, + ), + modifier = modifier + .height(52.dp) + .widthIn(min = 100.dp), + ) { + Text( + text = label, + style = PieceTheme.typography.bodyMSB, + ) + } +} + @Preview @Composable fun PreviewPieceSolidButton() { @@ -312,9 +341,23 @@ fun PreviewPieceIconButton() { @Preview @Composable -fun PreviewPieceRoundingButton() { +fun PreviewPieceRoundingSolidButton() { + PieceTheme { + PieceRoundingSolidButton( + label = "Label", + onClick = {}, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) + } +} + +@Preview +@Composable +fun PreviewPieceRoundingOutlinedButton() { PieceTheme { - PieceRoundingButton( + PieceRoundingOutlinedButton( label = "Label", onClick = {}, modifier = Modifier diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Dialog.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Dialog.kt index 716f125a..0bae598f 100644 --- a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Dialog.kt +++ b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Dialog.kt @@ -211,7 +211,8 @@ fun PieceImageDialog( imageUri: Any?, buttonLabel: String, onDismissRequest: () -> Unit, - onButtonClick: () -> Unit, + onApproveClick: () -> Unit = {}, + isApproveButtonShow: Boolean = true, ) { Dialog( onDismissRequest = onDismissRequest, @@ -239,13 +240,15 @@ fun PieceImageDialog( .size(180.dp), ) - PieceRoundingButton( - label = buttonLabel, - onClick = onButtonClick, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 10.dp), - ) + if (isApproveButtonShow) { + PieceRoundingSolidButton( + label = buttonLabel, + onClick = onApproveClick, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 10.dp), + ) + } } } } @@ -308,7 +311,7 @@ fun PreviewPieceImageDialog() { imageUri = R.drawable.ic_image_default, buttonLabel = "매칭 수락하기", onDismissRequest = {}, - onButtonClick = {}, + onApproveClick = {}, ) } } diff --git a/core/designsystem/src/main/res/drawable/ic_onboarding_matching.xml b/core/designsystem/src/main/res/drawable/ic_onboarding_matching.xml new file mode 100644 index 00000000..386a9bc0 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_onboarding_matching.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_right_disable.xml b/core/designsystem/src/main/res/drawable/ic_right_disable.xml new file mode 100644 index 00000000..23d46f44 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_right_disable.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index b7ff3846..98acaf42 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -48,6 +48,7 @@ 상대방이 매칭을 수락했어요! 상대방과 연결되었어요! 연락처 확인하기 + 사진 보기 서로의 빈 곳을 채우며 맞물리는 퍼즐처럼.\n서로의 가치관과 마음이 연결되는 순간을 만들어갑니다. @@ -81,9 +82,9 @@ 년생 cm - 활동 지역 - 직업 - 흡연 + 활동 지역 + 직업 + 흡연 몸무게 kg 전체 diff --git a/core/domain/src/main/java/com/puzzle/domain/model/profile/Contact.kt b/core/domain/src/main/java/com/puzzle/domain/model/profile/Contact.kt index af90442a..c172595a 100644 --- a/core/domain/src/main/java/com/puzzle/domain/model/profile/Contact.kt +++ b/core/domain/src/main/java/com/puzzle/domain/model/profile/Contact.kt @@ -1,11 +1,11 @@ package com.puzzle.domain.model.profile data class Contact( - val snsPlatform: SnsPlatform, + val type: ContactType, val content: String, ) -enum class SnsPlatform(val displayName: String) { +enum class ContactType(val displayName: String) { KAKAO_TALK_ID("카카오톡 아이디"), OPEN_CHAT_URL("카카오톡 오픈 채팅방"), INSTAGRAM_ID("인스타 아이디"), @@ -13,8 +13,8 @@ enum class SnsPlatform(val displayName: String) { UNKNOWN(""); companion object { - fun create(value: String): SnsPlatform { - return SnsPlatform.entries.firstOrNull { it.name == value } ?: UNKNOWN + fun create(value: String?): ContactType { + return ContactType.entries.firstOrNull { it.name == value } ?: UNKNOWN } } } diff --git a/core/domain/src/main/java/com/puzzle/domain/model/profile/MyProfile.kt b/core/domain/src/main/java/com/puzzle/domain/model/profile/MyProfile.kt new file mode 100644 index 00000000..c6b53e16 --- /dev/null +++ b/core/domain/src/main/java/com/puzzle/domain/model/profile/MyProfile.kt @@ -0,0 +1,36 @@ +package com.puzzle.domain.model.profile + +data class MyProfileBasic( + val description: String, + val nickname: String, + val age: Int, + val birthDate: String, + val height: Int, + val weight: Int, + val location: String, + val job: String, + val smokingStatus: String, + val snsActivityLevel: String, + val imageUrl: String, + val contacts: List, +) { + fun isSmoke(): Boolean = if (smokingStatus == "흡연") true else false + fun isSnsActive(): Boolean = if (snsActivityLevel == "활동") true else false +} + +data class MyValuePick( + val id: Int, + val category: String, + val question: String, + val answerOptions: List, + val selectedAnswer: Int, +) + +data class MyValueTalk( + val id: Int, + val category: String, + val title: String, + val answer: String, + val summary: String, + val guides: List, +) diff --git a/core/domain/src/main/java/com/puzzle/domain/model/profile/MyValuePick.kt b/core/domain/src/main/java/com/puzzle/domain/model/profile/MyValuePick.kt deleted file mode 100644 index c5af1f4e..00000000 --- a/core/domain/src/main/java/com/puzzle/domain/model/profile/MyValuePick.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.puzzle.domain.model.profile - -data class MyValuePick( - val id: Int, - val category: String, - val question: String, - val answerOptions: List, - val selectedAnswer: Int, -) diff --git a/core/domain/src/main/java/com/puzzle/domain/model/profile/MyValueTalk.kt b/core/domain/src/main/java/com/puzzle/domain/model/profile/MyValueTalk.kt deleted file mode 100644 index a4bad60a..00000000 --- a/core/domain/src/main/java/com/puzzle/domain/model/profile/MyValueTalk.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.puzzle.domain.model.profile - -data class MyValueTalk( - val id: Int, - val category: String, - val title: String, - val answer: String, - val guides: List, - val summary: String, -) diff --git a/core/domain/src/main/java/com/puzzle/domain/repository/ProfileRepository.kt b/core/domain/src/main/java/com/puzzle/domain/repository/ProfileRepository.kt index e44841c9..5a6dc5dd 100644 --- a/core/domain/src/main/java/com/puzzle/domain/repository/ProfileRepository.kt +++ b/core/domain/src/main/java/com/puzzle/domain/repository/ProfileRepository.kt @@ -1,6 +1,9 @@ package com.puzzle.domain.repository import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk import com.puzzle.domain.model.profile.ValuePickAnswer import com.puzzle.domain.model.profile.ValuePickQuestion import com.puzzle.domain.model.profile.ValueTalkAnswer @@ -13,6 +16,31 @@ interface ProfileRepository { suspend fun loadValueTalkQuestions(): Result suspend fun retrieveValueTalkQuestion(): Result> + suspend fun loadMyProfileBasic(): Result + suspend fun retrieveMyProfileBasic(): Result + + suspend fun loadMyValueTalks(): Result + suspend fun retrieveMyValueTalks(): Result> + + suspend fun loadMyValuePicks(): Result + suspend fun retrieveMyValuePicks(): Result> + + suspend fun updateMyValueTalks(valueTalks: List): Result> + suspend fun updateMyValuePicks(valuePicks: List): Result> + suspend fun updateMyProfileBasic( + description: String, + nickname: String, + birthDate: String, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, + snsActivityLevel: String, + imageUrl: String, + contacts: List, + ): Result + suspend fun checkNickname(nickname: String): Result suspend fun uploadProfile( birthdate: String, diff --git a/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyProfileBasicUseCase.kt b/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyProfileBasicUseCase.kt new file mode 100644 index 00000000..4b2656b5 --- /dev/null +++ b/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyProfileBasicUseCase.kt @@ -0,0 +1,16 @@ +package com.puzzle.domain.usecase.profile + +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.domain.repository.ProfileRepository +import javax.inject.Inject + +class GetMyProfileBasicUseCase @Inject constructor( + private val profileRepository: ProfileRepository, +) { + suspend operator fun invoke(): Result = + profileRepository.retrieveMyProfileBasic() + .recoverCatching { + profileRepository.loadMyProfileBasic().getOrThrow() + profileRepository.retrieveMyProfileBasic().getOrThrow() + } +} diff --git a/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyValuePicksUseCase.kt b/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyValuePicksUseCase.kt new file mode 100644 index 00000000..c04aa45d --- /dev/null +++ b/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyValuePicksUseCase.kt @@ -0,0 +1,16 @@ +package com.puzzle.domain.usecase.profile + +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.repository.ProfileRepository +import javax.inject.Inject + +class GetMyValuePicksUseCase @Inject constructor( + private val profileRepository: ProfileRepository, +) { + suspend operator fun invoke(): Result> = + profileRepository.retrieveMyValuePicks() + .recoverCatching { + profileRepository.loadMyValuePicks().getOrThrow() + profileRepository.retrieveMyValuePicks().getOrThrow() + } +} diff --git a/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyValueTalksUseCase.kt b/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyValueTalksUseCase.kt new file mode 100644 index 00000000..3ab15aca --- /dev/null +++ b/core/domain/src/main/java/com/puzzle/domain/usecase/profile/GetMyValueTalksUseCase.kt @@ -0,0 +1,16 @@ +package com.puzzle.domain.usecase.profile + +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.domain.repository.ProfileRepository +import javax.inject.Inject + +class GetMyValueTalksUseCase @Inject constructor( + private val profileRepository: ProfileRepository, +) { + suspend operator fun invoke(): Result> = + profileRepository.retrieveMyValueTalks() + .recoverCatching { + profileRepository.loadMyValueTalks().getOrThrow() + profileRepository.retrieveMyValueTalks().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 index 51c8769e..ed53ebd2 100644 --- 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 @@ -7,7 +7,6 @@ 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 @@ -19,21 +18,13 @@ class SpyMatchingRepository : MatchingRepository { 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")) - } + 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) } diff --git a/core/domain/src/test/kotlin/com/puzzle/domain/spy/repository/SpyProfileRepository.kt b/core/domain/src/test/kotlin/com/puzzle/domain/spy/repository/SpyProfileRepository.kt new file mode 100644 index 00000000..562d3c08 --- /dev/null +++ b/core/domain/src/test/kotlin/com/puzzle/domain/spy/repository/SpyProfileRepository.kt @@ -0,0 +1,149 @@ +package com.puzzle.domain.spy.repository + +import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.domain.model.profile.ValuePickAnswer +import com.puzzle.domain.model.profile.ValuePickQuestion +import com.puzzle.domain.model.profile.ValueTalkAnswer +import com.puzzle.domain.model.profile.ValueTalkQuestion +import com.puzzle.domain.repository.ProfileRepository + +class SpyProfileRepository : ProfileRepository { + private var localMyProfileBasic: MyProfileBasic? = null + private var localMyValueTalks: List? = null + private var localMyValuePicks: List? = null + + private var remoteMyProfileBasic: MyProfileBasic? = null + private var remoteMyValueTalks: List? = null + private var remoteMyValuePicks: List? = null + + var loadMyProfileBasicCallCount = 0 + private set + + var loadMyValueTalksCallCount = 0 + private set + + var loadMyValuePicksCallCount = 0 + private set + + fun setLocalMyProfileBasic(profileBasic: MyProfileBasic) { + localMyProfileBasic = profileBasic + } + + fun setLocalMyValueTalks(valueTalks: List) { + localMyValueTalks = valueTalks + } + + fun setLocalMyValuePicks(valuePicks: List) { + localMyValuePicks = valuePicks + } + + fun setRemoteMyProfileBasic(profileBasic: MyProfileBasic) { + remoteMyProfileBasic = profileBasic + } + + fun setRemoteMyValueTalks(valueTalks: List) { + remoteMyValueTalks = valueTalks + } + + fun setRemoteMyValuePicks(valuePicks: List) { + remoteMyValuePicks = valuePicks + } + + override suspend fun loadValuePickQuestions(): Result { + return Result.success(Unit) + } + + override suspend fun retrieveValuePickQuestion(): Result> { + TODO("Not yet implemented") + } + + override suspend fun loadValueTalkQuestions(): Result { + return Result.success(Unit) + } + + override suspend fun retrieveValueTalkQuestion(): Result> { + TODO("Not yet implemented") + } + + override suspend fun loadMyProfileBasic(): Result { + loadMyProfileBasicCallCount++ + localMyProfileBasic = remoteMyProfileBasic + return Result.success(Unit) + } + + override suspend fun retrieveMyProfileBasic(): Result = + localMyProfileBasic?.let { Result.success(it) } + ?: Result.failure(NoSuchElementException("No value present in DataStore")) + + + override suspend fun loadMyValueTalks(): Result { + loadMyValueTalksCallCount++ + localMyValueTalks = remoteMyValueTalks + return Result.success(Unit) + } + + override suspend fun retrieveMyValueTalks(): Result> = + localMyValueTalks?.let { Result.success(it) } + ?: Result.failure(NoSuchElementException("No value present in DataStore")) + + + override suspend fun loadMyValuePicks(): Result { + loadMyValuePicksCallCount++ + localMyValuePicks = remoteMyValuePicks + return Result.success(Unit) + } + + override suspend fun retrieveMyValuePicks(): Result> = + localMyValuePicks?.let { Result.success(it) } + ?: Result.failure(NoSuchElementException("No value present in DataStore")) + + + override suspend fun updateMyValueTalks(valueTalks: List): Result> { + TODO("Not yet implemented") + } + + override suspend fun updateMyValuePicks(valuePicks: List): Result> { + TODO("Not yet implemented") + } + + override suspend fun updateMyProfileBasic( + description: String, + nickname: String, + birthDate: String, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, + snsActivityLevel: String, + imageUrl: String, + contacts: List + ): Result { + TODO("Not yet implemented") + } + + override suspend fun checkNickname(nickname: String): Result { + TODO("Not yet implemented") + } + + override suspend fun uploadProfile( + birthdate: String, + description: String, + height: Int, + weight: Int, + imageUrl: String, + job: String, + location: String, + nickname: String, + smokingStatus: String, + snsActivityLevel: String, + contacts: List, + valuePicks: List, + valueTalks: List + ): 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 index 0d85f6ac..d661ccef 100644 --- 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 @@ -19,23 +19,7 @@ class GetOpponentProfileUseCaseTest { @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) + val remoteProfile = dummyOpponentProfile.copy(description = "Remote Profile") spyMatchingRepository.setRemoteOpponentProfile(remoteProfile) // When @@ -49,21 +33,7 @@ class GetOpponentProfileUseCaseTest { @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 localProfile = dummyOpponentProfile spyMatchingRepository.setLocalOpponentProfile(localProfile) // When @@ -73,4 +43,19 @@ class GetOpponentProfileUseCaseTest { assertEquals(localProfile, result.getOrNull()) assertEquals(0, spyMatchingRepository.loadOpponentProfileCallCount) } + + private val dummyOpponentProfile = 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" + ) } diff --git a/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyProfileBasicUseCaseTest.kt b/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyProfileBasicUseCaseTest.kt new file mode 100644 index 00000000..f25346fb --- /dev/null +++ b/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyProfileBasicUseCaseTest.kt @@ -0,0 +1,68 @@ +package com.puzzle.domain.usecase.profile + +import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.ContactType +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.domain.spy.repository.SpyProfileRepository +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 GetMyProfileBasicUseCaseTest { + private lateinit var spyProfileRepository: SpyProfileRepository + private lateinit var getMyProfileBasicUseCase: GetMyProfileBasicUseCase + + @BeforeEach + fun setup() { + spyProfileRepository = SpyProfileRepository() + getMyProfileBasicUseCase = GetMyProfileBasicUseCase(spyProfileRepository) + } + + @Test + fun `로컬에서 데이터를 불러오다가 실패했을 경우 서버 데이터를 불러온다`() = runTest { + // Given + val remoteProfile = dummyMyProfileBasic.copy(description = "Remote Profile") + spyProfileRepository.setRemoteMyProfileBasic(remoteProfile) + + // When + val result = getMyProfileBasicUseCase() + + // Then + assertEquals(remoteProfile, result.getOrNull()) + assertEquals(1, spyProfileRepository.loadMyProfileBasicCallCount) + } + + @Test + fun `로컬에서 데이터를 성공적으로 불러올 경우 서버 요청을 하지 않는다`() = runTest { + // Given + val localProfile = dummyMyProfileBasic + spyProfileRepository.setLocalMyProfileBasic(localProfile) + + // When + val result = getMyProfileBasicUseCase() + + // Then + assertEquals(localProfile, result.getOrNull()) + assertEquals(0, spyProfileRepository.loadMyProfileBasicCallCount) + } + + private val dummyMyProfileBasic = MyProfileBasic( + description = "새로운 인연을 만나고 싶은 26살 개발자입니다. 여행과 커피, 그리고 좋은 대화를 좋아해요.", + nickname = "코딩하는개발자", + age = 26, + birthDate = "1997-09-12", + height = 178, + weight = 72, + location = "서울 강남구", + job = "소프트웨어 개발자", + smokingStatus = "비흡연", + imageUrl = "https://example.com/profile/dev_profile.jpg", + contacts = listOf( + Contact(ContactType.KAKAO_TALK_ID, "dev_coder"), + Contact(ContactType.INSTAGRAM_ID, "seoul_dev_life"), + Contact(ContactType.PHONE_NUMBER, "010-1234-5678") + ), + snsActivityLevel = "은둔", + ) +} diff --git a/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyValuePicksUseCaseTest.kt b/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyValuePicksUseCaseTest.kt new file mode 100644 index 00000000..937fab19 --- /dev/null +++ b/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyValuePicksUseCaseTest.kt @@ -0,0 +1,77 @@ +package com.puzzle.domain.usecase.profile + +import com.puzzle.domain.model.profile.AnswerOption +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.spy.repository.SpyProfileRepository +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 GetMyValuePicksUseCaseTest { + private lateinit var spyProfileRepository: SpyProfileRepository + private lateinit var getMyValuePicksUseCase: GetMyValuePicksUseCase + + @BeforeEach + fun setup() { + spyProfileRepository = SpyProfileRepository() + getMyValuePicksUseCase = GetMyValuePicksUseCase(spyProfileRepository) + } + + @Test + fun `로컬에서 데이터를 불러오다가 실패했을 경우 서버 데이터를 불러온다`() = runTest { + // Given + val remoteValuePicks = + dummyValuePicks.map { it.copy(selectedAnswer = it.selectedAnswer + 1) } + + spyProfileRepository.setRemoteMyValuePicks(remoteValuePicks) + + // When + val result = getMyValuePicksUseCase() + + // Then + assertEquals(remoteValuePicks, result.getOrNull()) + assertEquals(1, spyProfileRepository.loadMyValuePicksCallCount) + } + + @Test + fun `로컬에서 데이터를 성공적으로 불러올 경우 서버 요청을 하지 않는다`() = runTest { + // Given + val localValuePicks = dummyValuePicks + spyProfileRepository.setLocalMyValuePicks(localValuePicks) + + // When + val result = getMyValuePicksUseCase() + + // Then + assertEquals(localValuePicks, result.getOrNull()) + assertEquals(0, spyProfileRepository.loadMyValuePicksCallCount) + } + + private val dummyValuePicks = listOf( + MyValuePick( + id = 1, + category = "인생의 가치", + question = "당신에게 가장 중요한 것은 무엇인가요?", + answerOptions = listOf( + AnswerOption(1, "성장과 배움"), + AnswerOption(2, "가족과 관계"), + AnswerOption(3, "자아실현"), + AnswerOption(4, "안정과 평화") + ), + selectedAnswer = 1 + ), + MyValuePick( + id = 2, + category = "연애관", + question = "좋은 파트너에게 가장 중요하게 생각하는 것은?", + answerOptions = listOf( + AnswerOption(1, "진실성"), + AnswerOption(2, "공감능력"), + AnswerOption(3, "지적 호기심"), + AnswerOption(4, "유머감각") + ), + selectedAnswer = 2 + ) + ) +} diff --git a/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyValueTalksUseCaseTest.kt b/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyValueTalksUseCaseTest.kt new file mode 100644 index 00000000..53a162a5 --- /dev/null +++ b/core/domain/src/test/kotlin/com/puzzle/domain/usecase/profile/GetMyValueTalksUseCaseTest.kt @@ -0,0 +1,66 @@ +package com.puzzle.domain.usecase.profile + +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.domain.spy.repository.SpyProfileRepository +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 GetMyValueTalksUseCaseTest { + private lateinit var spyProfileRepository: SpyProfileRepository + private lateinit var getMyValueTalksUseCase: GetMyValueTalksUseCase + + @BeforeEach + fun setup() { + spyProfileRepository = SpyProfileRepository() + getMyValueTalksUseCase = GetMyValueTalksUseCase(spyProfileRepository) + } + + @Test + fun `로컬에서 데이터를 불러오다가 실패했을 경우 서버 데이터를 불러온다`() = runTest { + // Given + val remoteValueTalks = dummyValueTalks.map { it.copy(summary = "Updated " + it.summary) } + spyProfileRepository.setRemoteMyValueTalks(remoteValueTalks) + + // When + val result = getMyValueTalksUseCase() + + // Then + assertEquals(remoteValueTalks, result.getOrNull()) + assertEquals(1, spyProfileRepository.loadMyValueTalksCallCount) + } + + @Test + fun `로컬에서 데이터를 성공적으로 불러올 경우 서버 요청을 하지 않는다`() = runTest { + // Given + val localValueTalks = dummyValueTalks + spyProfileRepository.setLocalMyValueTalks(localValueTalks) + + // When + val result = getMyValueTalksUseCase() + + // Then + assertEquals(localValueTalks, result.getOrNull()) + assertEquals(0, spyProfileRepository.loadMyValueTalksCallCount) + } + + private val dummyValueTalks = listOf( + MyValueTalk( + id = 1, + category = "커리어", + title = "나의 개발 여정", + answer = "대학에서 컴퓨터공학을 전공하고 스타트업에서 junior 개발자로 시작했어요. 꾸준한 학습과 도전을 통해 성장하고 있습니다.", + summary = "기술에 대한 열정과 성장 마인드셋", + guides = emptyList(), + ), + MyValueTalk( + id = 2, + category = "라이프스타일", + title = "일과 삶의 균형", + answer = "개발 공부와 개인적인 성장, 취미 활동 사이의 균형을 중요하게 생각해요. 주말에는 주로 카페에서 책을 읽거나 새로운 기술을 공부합니다.", + summary = "균형 잡힌 삶을 추구하는 개발자", + guides = emptyList(), + ) + ) +} diff --git a/core/navigation/src/main/java/com/puzzle/navigation/Route.kt b/core/navigation/src/main/java/com/puzzle/navigation/Route.kt index caa3072a..0f9f947a 100644 --- a/core/navigation/src/main/java/com/puzzle/navigation/Route.kt +++ b/core/navigation/src/main/java/com/puzzle/navigation/Route.kt @@ -46,7 +46,10 @@ sealed class MatchingGraphDest : Route { data class BlockRoute(val userId: Int, val userName: String) : MatchingGraphDest() @Serializable - data object ContactRoute: MatchingGraphDest() + data object ContactRoute : MatchingGraphDest() + + @Serializable + data object ProfilePreviewRoute : MatchingGraphDest() } @Serializable 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 85ee07b1..9a379d67 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 @@ -12,9 +12,15 @@ 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 +import com.puzzle.network.model.profile.GetMyProfileBasicResponse +import com.puzzle.network.model.profile.GetMyValuePicksResponse +import com.puzzle.network.model.profile.GetMyValueTalksResponse +import com.puzzle.network.model.profile.LoadValuePickQuestionsResponse +import com.puzzle.network.model.profile.LoadValueTalkQuestionsResponse +import com.puzzle.network.model.profile.UpdateMyProfileBasicRequest +import com.puzzle.network.model.profile.UpdateMyValuePickRequests +import com.puzzle.network.model.profile.UpdateMyValueTalkRequests import com.puzzle.network.model.profile.UploadProfileRequest import com.puzzle.network.model.profile.UploadProfileResponse import com.puzzle.network.model.terms.AgreeTermsRequest @@ -27,6 +33,7 @@ import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.PATCH import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Part import retrofit2.http.Path import retrofit2.http.Query @@ -45,10 +52,10 @@ interface PieceApi { suspend fun loadTerms(): Result> @GET("/api/valuePicks") - suspend fun loadValuePickQuestions(): Result> + suspend fun loadValuePickQuestions(): Result> @GET("/api/valueTalks") - suspend fun loadValueTalkQuestions(): Result> + suspend fun loadValueTalkQuestions(): Result> @GET("/api/login/token/health-check") suspend fun checkTokenHealth(@Query("token") token: String): Result> @@ -78,6 +85,24 @@ interface PieceApi { @POST("/api/blockContacts") suspend fun blockContacts(@Body blockContactsRequest: BlockContactsRequest): Result> + @GET("/api/profiles/valueTalks") + suspend fun getMyValueTalks(): Result> + + @GET("/api/profiles/valuePicks") + suspend fun getMyValuePicks(): Result> + + @GET("/api/profiles/basic") + suspend fun getMyProfileBasic(): Result> + + @PUT("/api/profiles/valueTalks") + suspend fun updateMyValueTalks(@Body updateMyValueTalkRequests: UpdateMyValueTalkRequests): Result> + + @PUT("/api/profiles/valuePicks") + suspend fun updateMyValuePicks(@Body updateMyValuePickRequests: UpdateMyValuePickRequests): Result> + + @PUT("/api/profiles/basic") + suspend fun updateMyProfileBasic(@Body updateMyProfileBasicRequest: UpdateMyProfileBasicRequest): Result> + @GET("/api/matches/infos") suspend fun getMatchInfo(): Result> 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 index 3dfad065..77b42f9d 100644 --- 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 @@ -3,6 +3,7 @@ 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 com.puzzle.network.model.profile.ValuePickAnswerResponse import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/GetMyProfileBasicResponse.kt b/core/network/src/main/java/com/puzzle/network/model/profile/GetMyProfileBasicResponse.kt new file mode 100644 index 00000000..52e85569 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/GetMyProfileBasicResponse.kt @@ -0,0 +1,50 @@ +package com.puzzle.network.model.profile + +import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.ContactType +import com.puzzle.domain.model.profile.MyProfileBasic +import com.puzzle.network.model.UNKNOWN_INT +import com.puzzle.network.model.UNKNOWN_STRING +import kotlinx.serialization.Serializable + +@Serializable +data class GetMyProfileBasicResponse( + val description: String?, + val nickname: String?, + val age: Int?, + val birthDate: String?, + val height: Int?, + val weight: Int?, + val location: String?, + val job: String?, + val smokingStatus: String?, + val snsActivityLevel: String?, + val imageUrl: String?, + val contacts: List?, +) { + fun toDomain() = MyProfileBasic( + description = description ?: UNKNOWN_STRING, + nickname = nickname ?: UNKNOWN_STRING, + age = age ?: UNKNOWN_INT, + birthDate = birthDate ?: UNKNOWN_STRING, + height = height ?: UNKNOWN_INT, + weight = weight ?: UNKNOWN_INT, + location = location ?: UNKNOWN_STRING, + job = job ?: UNKNOWN_STRING, + smokingStatus = smokingStatus ?: UNKNOWN_STRING, + snsActivityLevel = snsActivityLevel ?: UNKNOWN_STRING, + imageUrl = imageUrl ?: UNKNOWN_STRING, + contacts = contacts?.map(ContactResponse::toDomain) ?: emptyList() + ) +} + +@Serializable +data class ContactResponse( + val type: String?, + val value: String?, +) { + fun toDomain() = Contact( + type = ContactType.create(type), + content = value ?: UNKNOWN_STRING, + ) +} diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/GetMyValuePicksResponse.kt b/core/network/src/main/java/com/puzzle/network/model/profile/GetMyValuePicksResponse.kt new file mode 100644 index 00000000..8d123d28 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/GetMyValuePicksResponse.kt @@ -0,0 +1,31 @@ +package com.puzzle.network.model.profile + +import com.puzzle.domain.model.profile.MyValuePick +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 GetMyValuePicksResponse( + val response: List?, +) { + fun toDomain() = response?.map(MyValuePickResponse::toDomain) ?: emptyList() +} + +@Serializable +data class MyValuePickResponse( + @SerialName("profileValuePickId") val id: Int?, + val category: String?, + val question: String?, + @SerialName("answer") val answerOptions: List?, + val selectedAnswer: Int?, +) { + fun toDomain() = MyValuePick( + id = id ?: UNKNOWN_INT, + category = category ?: UNKNOWN_STRING, + question = question ?: UNKNOWN_STRING, + answerOptions = answerOptions?.map(ValuePickAnswerResponse::toDomain) ?: emptyList(), + selectedAnswer = selectedAnswer ?: UNKNOWN_INT, + ) +} diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/GetMyValueTalksResponse.kt b/core/network/src/main/java/com/puzzle/network/model/profile/GetMyValueTalksResponse.kt new file mode 100644 index 00000000..2f85521a --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/GetMyValueTalksResponse.kt @@ -0,0 +1,33 @@ +package com.puzzle.network.model.profile + +import com.puzzle.domain.model.profile.MyValueTalk +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 GetMyValueTalksResponse( + val response: List?, +) { + fun toDomain() = response?.map(MyValueTalkResponse::toDomain) ?: emptyList() +} + +@Serializable +data class MyValueTalkResponse( + @SerialName("profileValueTalkId") val id: Int?, + val title: String?, + val category: String?, + val answer: String?, + val summary: String?, + val guides: List?, +) { + fun toDomain() = MyValueTalk( + id = id ?: UNKNOWN_INT, + title = title ?: UNKNOWN_STRING, + category = category ?: UNKNOWN_STRING, + answer = answer ?: UNKNOWN_STRING, + summary = summary ?: UNKNOWN_STRING, + guides = guides ?: emptyList(), + ) +} diff --git a/core/network/src/main/java/com/puzzle/network/model/matching/LoadValuePicksResponse.kt b/core/network/src/main/java/com/puzzle/network/model/profile/LoadValuePickQuestionsResponse.kt similarity index 92% rename from core/network/src/main/java/com/puzzle/network/model/matching/LoadValuePicksResponse.kt rename to core/network/src/main/java/com/puzzle/network/model/profile/LoadValuePickQuestionsResponse.kt index ee53a418..2bd61b28 100644 --- a/core/network/src/main/java/com/puzzle/network/model/matching/LoadValuePicksResponse.kt +++ b/core/network/src/main/java/com/puzzle/network/model/profile/LoadValuePickQuestionsResponse.kt @@ -1,4 +1,4 @@ -package com.puzzle.network.model.matching +package com.puzzle.network.model.profile import com.puzzle.domain.model.profile.AnswerOption import com.puzzle.domain.model.profile.ValuePickQuestion @@ -7,7 +7,7 @@ import com.puzzle.network.model.UNKNOWN_STRING import kotlinx.serialization.Serializable @Serializable -data class LoadValuePicksResponse( +data class LoadValuePickQuestionsResponse( val responses: List?, ) { fun toDomain() = responses?.map(ValuePickResponse::toDomain) ?: emptyList() diff --git a/core/network/src/main/java/com/puzzle/network/model/matching/LoadValueTalksResponse.kt b/core/network/src/main/java/com/puzzle/network/model/profile/LoadValueTalkQuestionsResponse.kt similarity index 89% rename from core/network/src/main/java/com/puzzle/network/model/matching/LoadValueTalksResponse.kt rename to core/network/src/main/java/com/puzzle/network/model/profile/LoadValueTalkQuestionsResponse.kt index e43a2a53..f4ce0ef8 100644 --- a/core/network/src/main/java/com/puzzle/network/model/matching/LoadValueTalksResponse.kt +++ b/core/network/src/main/java/com/puzzle/network/model/profile/LoadValueTalkQuestionsResponse.kt @@ -1,4 +1,4 @@ -package com.puzzle.network.model.matching +package com.puzzle.network.model.profile import com.puzzle.domain.model.profile.ValueTalkQuestion import com.puzzle.network.model.UNKNOWN_INT @@ -6,7 +6,7 @@ import com.puzzle.network.model.UNKNOWN_STRING import kotlinx.serialization.Serializable @Serializable -data class LoadValueTalksResponse( +data class LoadValueTalkQuestionsResponse( val responses: List?, ) { fun toDomain() = responses?.map(ValueTalkResponse::toDomain) ?: emptyList() diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyProfileBasicRequest.kt b/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyProfileBasicRequest.kt new file mode 100644 index 00000000..6a9a615e --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyProfileBasicRequest.kt @@ -0,0 +1,18 @@ +package com.puzzle.network.model.profile + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateMyProfileBasicRequest( + val description: String, + val nickname: String, + val birthDate: String, + val height: Int, + val weight: Int, + val location: String, + val job: String, + val smokingStatus: String, + val snsActivityLevel: String, + val imageUrl: String, + val contacts: Map, +) diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyValuePickRequest.kt b/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyValuePickRequest.kt new file mode 100644 index 00000000..dcd88c31 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyValuePickRequest.kt @@ -0,0 +1,14 @@ +package com.puzzle.network.model.profile + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateMyValuePickRequests( + val profileValueTalkUpdateRequests: List, +) + +@Serializable +data class UpdateMyValuePickRequest( + val profileValuePickId: Int, + val selectedAnswer: Int, +) diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyValueTalkRequest.kt b/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyValueTalkRequest.kt new file mode 100644 index 00000000..d32a05dd --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/UpdateMyValueTalkRequest.kt @@ -0,0 +1,15 @@ +package com.puzzle.network.model.profile + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateMyValueTalkRequests( + val profileValueTalkUpdateRequests: List, +) + +@Serializable +data class UpdateMyValueTalkRequest( + val profileValueTalkId: Int, + val answer: String, + val summary: String, +) diff --git a/core/network/src/main/java/com/puzzle/network/source/profile/ProfileDataSource.kt b/core/network/src/main/java/com/puzzle/network/source/profile/ProfileDataSource.kt index 9e14629d..fbc3a280 100644 --- a/core/network/src/main/java/com/puzzle/network/source/profile/ProfileDataSource.kt +++ b/core/network/src/main/java/com/puzzle/network/source/profile/ProfileDataSource.kt @@ -1,17 +1,44 @@ package com.puzzle.network.source.profile import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk import com.puzzle.domain.model.profile.ValuePickAnswer import com.puzzle.domain.model.profile.ValueTalkAnswer -import com.puzzle.network.model.matching.LoadValuePicksResponse -import com.puzzle.network.model.matching.LoadValueTalksResponse +import com.puzzle.network.model.profile.GetMyProfileBasicResponse +import com.puzzle.network.model.profile.GetMyValuePicksResponse +import com.puzzle.network.model.profile.GetMyValueTalksResponse +import com.puzzle.network.model.profile.LoadValuePickQuestionsResponse +import com.puzzle.network.model.profile.LoadValueTalkQuestionsResponse import com.puzzle.network.model.profile.UploadProfileResponse import java.io.InputStream interface ProfileDataSource { - suspend fun loadValuePickQuestions(): Result - suspend fun loadValueTalkQuestions(): Result + suspend fun loadValuePickQuestions(): Result + suspend fun loadValueTalkQuestions(): Result + + suspend fun getMyProfileBasic(): Result + suspend fun getMyValueTalks(): Result + suspend fun getMyValuePicks(): Result + + suspend fun updateMyValueTalks(valueTalks: List): Result + suspend fun updateMyValuePicks(valuePicks: List): Result + suspend fun updateMyProfileBasic( + description: String, + nickname: String, + birthDate: String, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, + snsActivityLevel: String, + imageUrl: String, + contacts: List, + ): Result + suspend fun checkNickname(nickname: String): Result + suspend fun uploadProfileImage(imageInputStream: InputStream): Result suspend fun uploadProfile( birthdate: String, diff --git a/core/network/src/main/java/com/puzzle/network/source/profile/ProfileDataSourceImpl.kt b/core/network/src/main/java/com/puzzle/network/source/profile/ProfileDataSourceImpl.kt index bd3e54b9..6bf5db49 100644 --- a/core/network/src/main/java/com/puzzle/network/source/profile/ProfileDataSourceImpl.kt +++ b/core/network/src/main/java/com/puzzle/network/source/profile/ProfileDataSourceImpl.kt @@ -2,11 +2,21 @@ package com.puzzle.network.source.profile import android.os.Build import com.puzzle.domain.model.profile.Contact +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.model.profile.MyValueTalk import com.puzzle.domain.model.profile.ValuePickAnswer import com.puzzle.domain.model.profile.ValueTalkAnswer import com.puzzle.network.api.PieceApi -import com.puzzle.network.model.matching.LoadValuePicksResponse -import com.puzzle.network.model.matching.LoadValueTalksResponse +import com.puzzle.network.model.profile.GetMyProfileBasicResponse +import com.puzzle.network.model.profile.GetMyValuePicksResponse +import com.puzzle.network.model.profile.GetMyValueTalksResponse +import com.puzzle.network.model.profile.LoadValuePickQuestionsResponse +import com.puzzle.network.model.profile.LoadValueTalkQuestionsResponse +import com.puzzle.network.model.profile.UpdateMyProfileBasicRequest +import com.puzzle.network.model.profile.UpdateMyValuePickRequest +import com.puzzle.network.model.profile.UpdateMyValuePickRequests +import com.puzzle.network.model.profile.UpdateMyValueTalkRequest +import com.puzzle.network.model.profile.UpdateMyValueTalkRequests import com.puzzle.network.model.profile.UploadProfileRequest import com.puzzle.network.model.profile.UploadProfileResponse import com.puzzle.network.model.profile.ValuePickAnswerRequest @@ -24,16 +34,80 @@ import javax.inject.Singleton class ProfileDataSourceImpl @Inject constructor( private val pieceApi: PieceApi, ) : ProfileDataSource { - override suspend fun loadValuePickQuestions(): Result = + override suspend fun loadValuePickQuestions(): Result = pieceApi.loadValuePickQuestions().unwrapData() - override suspend fun loadValueTalkQuestions(): Result = + override suspend fun loadValueTalkQuestions(): Result = pieceApi.loadValueTalkQuestions().unwrapData() - override suspend fun checkNickname(nickname: String): Result = + override suspend fun getMyProfileBasic(): Result = + pieceApi.getMyProfileBasic().unwrapData() + + override suspend fun getMyValueTalks(): Result = + pieceApi.getMyValueTalks().unwrapData() + + override suspend fun getMyValuePicks(): Result = + pieceApi.getMyValuePicks().unwrapData() + + override suspend fun updateMyValueTalks(valueTalks: List): Result = + pieceApi.updateMyValueTalks( + UpdateMyValueTalkRequests( + valueTalks.map { + UpdateMyValueTalkRequest( + profileValueTalkId = it.id, + answer = it.answer, + summary = it.summary, + ) + } + ) + ).unwrapData() + + override suspend fun updateMyValuePicks(valuePicks: List): Result = + pieceApi.updateMyValuePicks( + UpdateMyValuePickRequests( + valuePicks.map { + UpdateMyValuePickRequest( + profileValuePickId = it.id, + selectedAnswer = it.selectedAnswer, + ) + } + ) + ).unwrapData() + + override suspend fun updateMyProfileBasic( + description: String, + nickname: String, + birthDate: String, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, + snsActivityLevel: String, + imageUrl: String, + contacts: List, + ): Result = pieceApi.updateMyProfileBasic( + UpdateMyProfileBasicRequest( + birthDate = birthDate, + description = description, + height = height, + weight = weight, + imageUrl = imageUrl, + job = job, + location = location, + nickname = nickname, + smokingStatus = smokingStatus, + snsActivityLevel = snsActivityLevel, + contacts = contacts.associate { it.type.name to it.content }, + ) + ).unwrapData() + + override suspend + fun checkNickname(nickname: String): Result = pieceApi.checkNickname(nickname).unwrapData() - override suspend fun uploadProfileImage(imageInputStream: InputStream): Result { + override suspend + fun uploadProfileImage(imageInputStream: InputStream): Result { val (imageFileExtension, imageFileName) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { WEBP_MEDIA_TYPE to "profile_${UUID.randomUUID()}.webp" } else { @@ -52,7 +126,8 @@ class ProfileDataSourceImpl @Inject constructor( return pieceApi.uploadProfileImage(requestImage).unwrapData() } - override suspend fun uploadProfile( + override suspend + fun uploadProfile( birthdate: String, description: String, height: Int, @@ -90,7 +165,7 @@ class ProfileDataSourceImpl @Inject constructor( answer = it.answer, ) }, - contacts = contacts.associate { it.snsPlatform.name to it.content } + contacts = contacts.associate { it.type.name to it.content } ) ).unwrapData() diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/contact/ContactScreen.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/contact/ContactScreen.kt index 80a7ea5a..7ab67bd1 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/contact/ContactScreen.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/contact/ContactScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager @@ -41,7 +40,7 @@ import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceSubCloseTopBar import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.domain.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType import com.puzzle.matching.graph.contact.contract.ContactIntent import com.puzzle.matching.graph.contact.contract.ContactSideEffect import com.puzzle.matching.graph.contact.contract.ContactState @@ -155,8 +154,8 @@ private fun ContactInfo( onContactClick: (Contact) -> Unit, modifier: Modifier = Modifier, ) { - val contactIconId: Int? = selectedContact.snsPlatform.getContactIconId() - val contactNameId: Int? = selectedContact.snsPlatform.getContactNameId() + val contactIconId: Int? = selectedContact.type.getContactIconId() + val contactNameId: Int? = selectedContact.type.getContactNameId() val clipboardManager: ClipboardManager = LocalClipboardManager.current Column( @@ -217,9 +216,9 @@ private fun ContactInfo( contacts.forEach { val contactOnOffIconId = if (selectedContact == it) { - it.snsPlatform.getSelectedContactIconId() + it.type.getSelectedContactIconId() } else { - it.snsPlatform.getUnSelectedContactIconId() + it.type.getUnSelectedContactIconId() } if (contactOnOffIconId != null) { @@ -257,24 +256,24 @@ private fun ContactScreenPreview() { nickName = "수줍은 수달", contacts = listOf( Contact( - snsPlatform = SnsPlatform.KAKAO_TALK_ID, + type = ContactType.KAKAO_TALK_ID, content = "Puzzle1234" ), Contact( - snsPlatform = SnsPlatform.OPEN_CHAT_URL, + type = ContactType.OPEN_CHAT_URL, content = "Puzzle1234" ), Contact( - snsPlatform = SnsPlatform.INSTAGRAM_ID, + type = ContactType.INSTAGRAM_ID, content = "Puzzle1234" ), Contact( - snsPlatform = SnsPlatform.PHONE_NUMBER, + type = ContactType.PHONE_NUMBER, content = "Puzzle1234" ) ), selectedContact = Contact( - snsPlatform = SnsPlatform.OPEN_CHAT_URL, + type = ContactType.OPEN_CHAT_URL, content = "Puzzle1234" ), ), @@ -283,4 +282,4 @@ private fun ContactScreenPreview() { modifier = Modifier.fillMaxSize(), ) } -} \ No newline at end of file +} diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/contact/contract/ContactState.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/contact/contract/ContactState.kt index 800913f9..633bb4fd 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/contact/contract/ContactState.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/contact/contract/ContactState.kt @@ -3,7 +3,7 @@ package com.puzzle.matching.graph.contact.contract import com.airbnb.mvrx.MavericksState import com.puzzle.designsystem.R import com.puzzle.domain.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType data class ContactState( val isLoading: Boolean = false, @@ -12,34 +12,34 @@ data class ContactState( val selectedContact: Contact? = null, ) : MavericksState -internal fun SnsPlatform.getContactIconId(): Int? = when (this) { - SnsPlatform.KAKAO_TALK_ID -> R.drawable.ic_sns_kakao - SnsPlatform.OPEN_CHAT_URL -> R.drawable.ic_sns_openchatting - SnsPlatform.INSTAGRAM_ID -> R.drawable.ic_sns_instagram - SnsPlatform.PHONE_NUMBER -> R.drawable.ic_sns_call - SnsPlatform.UNKNOWN -> null +internal fun ContactType.getContactIconId(): Int? = when (this) { + ContactType.KAKAO_TALK_ID -> R.drawable.ic_sns_kakao + ContactType.OPEN_CHAT_URL -> R.drawable.ic_sns_openchatting + ContactType.INSTAGRAM_ID -> R.drawable.ic_sns_instagram + ContactType.PHONE_NUMBER -> R.drawable.ic_sns_call + ContactType.UNKNOWN -> null } -internal fun SnsPlatform.getContactNameId(): Int? = when (this) { - SnsPlatform.KAKAO_TALK_ID -> R.string.maching_contact_kakao_id - SnsPlatform.OPEN_CHAT_URL -> R.string.maching_contact_open_chat_id - SnsPlatform.INSTAGRAM_ID -> R.string.maching_contact_insta_id - SnsPlatform.PHONE_NUMBER -> R.string.maching_contact_phone_number - SnsPlatform.UNKNOWN -> null +internal fun ContactType.getContactNameId(): Int? = when (this) { + ContactType.KAKAO_TALK_ID -> R.string.maching_contact_kakao_id + ContactType.OPEN_CHAT_URL -> R.string.maching_contact_open_chat_id + ContactType.INSTAGRAM_ID -> R.string.maching_contact_insta_id + ContactType.PHONE_NUMBER -> R.string.maching_contact_phone_number + ContactType.UNKNOWN -> null } -internal fun SnsPlatform.getSelectedContactIconId(): Int? = when (this) { - SnsPlatform.KAKAO_TALK_ID -> R.drawable.ic_kakao_on - SnsPlatform.OPEN_CHAT_URL -> R.drawable.ic_open_chat_on - SnsPlatform.INSTAGRAM_ID -> R.drawable.ic_insta_on - SnsPlatform.PHONE_NUMBER -> R.drawable.ic_phone_on - SnsPlatform.UNKNOWN -> null +internal fun ContactType.getSelectedContactIconId(): Int? = when (this) { + ContactType.KAKAO_TALK_ID -> R.drawable.ic_kakao_on + ContactType.OPEN_CHAT_URL -> R.drawable.ic_open_chat_on + ContactType.INSTAGRAM_ID -> R.drawable.ic_insta_on + ContactType.PHONE_NUMBER -> R.drawable.ic_phone_on + ContactType.UNKNOWN -> null } -internal fun SnsPlatform.getUnSelectedContactIconId(): Int? = when (this) { - SnsPlatform.KAKAO_TALK_ID -> R.drawable.ic_kakao_off - SnsPlatform.OPEN_CHAT_URL -> R.drawable.ic_open_chat_off - SnsPlatform.INSTAGRAM_ID -> R.drawable.ic_insta_off - SnsPlatform.PHONE_NUMBER -> R.drawable.ic_phone_off - SnsPlatform.UNKNOWN -> null +internal fun ContactType.getUnSelectedContactIconId(): Int? = when (this) { + ContactType.KAKAO_TALK_ID -> R.drawable.ic_kakao_off + ContactType.OPEN_CHAT_URL -> R.drawable.ic_open_chat_off + ContactType.INSTAGRAM_ID -> R.drawable.ic_insta_off + ContactType.PHONE_NUMBER -> R.drawable.ic_phone_off + ContactType.UNKNOWN -> null } 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 6b80856e..90b3efee 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 @@ -43,7 +43,7 @@ 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.PieceRoundingSolidButton import com.puzzle.designsystem.component.PieceSubCloseTopBar import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.domain.model.profile.OpponentProfile @@ -171,7 +171,7 @@ private fun MatchingDetailScreen( PieceImageDialog( imageUri = state.profile?.imageUrl, buttonLabel = "매칭 수락하기", - onButtonClick = { dialogType = DialogType.ACCEPT_MATCHING }, + onApproveClick = { dialogType = DialogType.ACCEPT_MATCHING }, onDismissRequest = { showDialog = false }, ) } @@ -256,14 +256,12 @@ private fun MatchingDetailScreen( @Composable private fun BackgroundImage(modifier: Modifier = Modifier) { - Box(modifier = modifier.fillMaxSize()) { - Image( - painter = painterResource(id = R.drawable.matchingdetail_bg), - contentDescription = "basic info 배경화면", - contentScale = ContentScale.Crop, - modifier = Modifier.matchParentSize(), - ) - } + Image( + painter = painterResource(id = R.drawable.matchingdetail_bg), + contentDescription = "basic info 배경화면", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) } @Composable @@ -291,8 +289,8 @@ private fun MatchingDetailContent( age = profile.age, height = profile.height, weight = profile.weight, - activityRegion = profile.location, - occupation = profile.job, + location = profile.location, + job = profile.job, smokingStatus = profile.smokingStatus, onMoreClick = onMoreClick, ) @@ -367,7 +365,7 @@ private fun MatchingDetailBottomBar( Spacer(modifier = Modifier.width(8.dp)) if (currentPage == MatchingDetailPage.ValuePickPage) { - PieceRoundingButton( + PieceRoundingSolidButton( label = stringResource(R.string.accept_matching), onClick = onAcceptClick, ) diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/component/BasicInfoHeader.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/component/BasicInfoHeader.kt index c194dc1d..887acd78 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/component/BasicInfoHeader.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/detail/common/component/BasicInfoHeader.kt @@ -1,6 +1,7 @@ package com.puzzle.matching.graph.detail.common.component import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.puzzle.common.ui.clickable import com.puzzle.designsystem.R @@ -19,8 +21,9 @@ import com.puzzle.designsystem.foundation.PieceTheme internal fun BasicInfoHeader( nickName: String, selfDescription: String, - onMoreClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + hasMoreButton: Boolean = true, + onMoreClick: () -> Unit = {} ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier) { Text( @@ -37,13 +40,29 @@ internal fun BasicInfoHeader( modifier = Modifier.weight(1f) ) - Image( - painter = painterResource(id = R.drawable.ic_more), - contentDescription = "basic info 배경화면", - modifier = Modifier - .size(32.dp) - .clickable { onMoreClick() }, - ) + if (hasMoreButton) { + Image( + painter = painterResource(id = R.drawable.ic_more), + contentDescription = "basic info 배경화면", + modifier = Modifier + .size(32.dp) + .clickable { onMoreClick() }, + ) + } } } } + +@Preview +@Composable +private fun BasicInfoHeaderPreview() { + PieceTheme { + BasicInfoHeader( + nickName = "nickName", + selfDescription = "selfDescription", + onMoreClick = {}, + hasMoreButton = false, + modifier = Modifier.background(PieceTheme.colors.white) + ) + } +} 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 d781034f..e4119238 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 @@ -33,8 +33,8 @@ internal fun BasicInfoPage( age: Int, height: Int, weight: Int, - activityRegion: String, - occupation: String, + location: String, + job: String, smokingStatus: String, onMoreClick: () -> Unit, modifier: Modifier = Modifier, @@ -54,9 +54,9 @@ internal fun BasicInfoPage( birthYear = birthYear, height = height, weight = weight, - activityRegion = activityRegion, - occupation = occupation, - smokeStatue = smokingStatus, + location = location, + job = job, + smokingStatus = smokingStatus, modifier = Modifier.padding(horizontal = 20.dp) ) } @@ -92,9 +92,9 @@ private fun BasicInfoCard( birthYear: String, height: Int, weight: Int, - activityRegion: String, - occupation: String, - smokeStatue: String, + location: String, + job: String, + smokingStatus: String, modifier: Modifier = Modifier, ) { Row( @@ -191,8 +191,8 @@ private fun BasicInfoCard( .padding(bottom = 12.dp) ) { InfoItem( - title = stringResource(R.string.basicinfocard_activityRegion), - content = activityRegion, + title = stringResource(R.string.basicinfocard_location), + content = location, modifier = Modifier.size( width = 144.dp, height = 80.dp, @@ -200,14 +200,14 @@ private fun BasicInfoCard( ) InfoItem( - title = stringResource(R.string.basicinfocard_occupation), - content = occupation, + title = stringResource(R.string.basicinfocard_job), + content = job, modifier = Modifier.weight(1f), ) InfoItem( - title = stringResource(R.string.basicinfocard_smokeStatue), - content = smokeStatue, + title = stringResource(R.string.basicinfocard_smokingStatus), + content = smokingStatus, modifier = Modifier.weight(1f), ) } @@ -262,8 +262,8 @@ private fun ProfileBasicInfoPagePreview() { age = 31, height = 200, weight = 72, - activityRegion = "서울특별시", - occupation = "개발자", + location = "서울특별시", + job = "개발자", 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 6e237bee..524e39ee 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 @@ -9,6 +9,7 @@ import com.puzzle.domain.model.error.ErrorHelper import com.puzzle.domain.model.match.MatchStatus import com.puzzle.domain.model.user.UserRole import com.puzzle.domain.repository.MatchingRepository +import com.puzzle.domain.repository.ProfileRepository import com.puzzle.domain.repository.UserRepository import com.puzzle.matching.graph.main.contract.MatchingIntent import com.puzzle.matching.graph.main.contract.MatchingSideEffect @@ -26,11 +27,12 @@ import kotlinx.coroutines.launch class MatchingViewModel @AssistedInject constructor( @Assisted initialState: MatchingState, - private val navigationHelper: NavigationHelper, private val matchingRepository: MatchingRepository, + private val profileRepository: ProfileRepository, private val userRepository: UserRepository, internal val eventHelper: EventHelper, private val errorHelper: ErrorHelper, + private val navigationHelper: NavigationHelper, ) : MavericksViewModel(initialState) { private val intents = Channel(BUFFERED) private val _sideEffects = Channel(BUFFERED) @@ -61,15 +63,37 @@ class MatchingViewModel @AssistedInject constructor( } setState { copy(userRole = userRole) } + + // MatchingHome 화면에서 사전에 내 프로필 데이터를 케싱해놓습니다. + loadMyProfile() } .onFailure { errorHelper.sendError(it) } } + private fun loadMyProfile() = viewModelScope.launch { + val profileBasicJob = launch { + profileRepository.loadMyProfileBasic() + .onFailure { errorHelper.sendError(it) } + } + val valueTalksJob = launch { + profileRepository.loadMyValuePicks() + .onFailure { errorHelper.sendError(it) } + } + val valuePicksJob = launch { + profileRepository.loadMyValueTalks() + .onFailure { errorHelper.sendError(it) } + } + + profileBasicJob.join() + valueTalksJob.join() + valuePicksJob.join() + } + private suspend fun getMatchInfo() = matchingRepository.getMatchInfo() .onSuccess { setState { copy(matchInfo = it) } - // MatchingHome화면에서 사전에 MatchingDetail에서 필요한 데이터를 케싱해놓습니다. + // MatchingHome 화면에서 사전에 MatchingDetail에서 필요한 데이터를 케싱해놓습니다. matchingRepository.loadOpponentProfile() .onFailure { errorHelper.sendError(it) } } diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/main/page/MatchingWaitingScreen.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/main/page/MatchingWaitingScreen.kt index a018ee33..22c97bfe 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/graph/main/page/MatchingWaitingScreen.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/main/page/MatchingWaitingScreen.kt @@ -102,7 +102,7 @@ internal fun MatchingWaitingScreen( Spacer(modifier = Modifier.weight(1f)) Image( - painter = painterResource(R.drawable.ic_user_pending_home), + painter = painterResource(R.drawable.ic_onboarding_matching), contentDescription = null, modifier = Modifier .size(260.dp) diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/preview/ProfilePreviewScreen.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/ProfilePreviewScreen.kt new file mode 100644 index 00000000..a939d8e0 --- /dev/null +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/ProfilePreviewScreen.kt @@ -0,0 +1,316 @@ +package com.puzzle.matching.graph.preview + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel +import com.puzzle.common.ui.clickable +import com.puzzle.common.ui.repeatOnStarted +import com.puzzle.designsystem.R +import com.puzzle.designsystem.component.PieceImageDialog +import com.puzzle.designsystem.component.PieceLoading +import com.puzzle.designsystem.component.PieceRoundingOutlinedButton +import com.puzzle.designsystem.component.PieceSubCloseTopBar +import com.puzzle.designsystem.foundation.PieceTheme +import com.puzzle.domain.model.profile.OpponentProfile +import com.puzzle.matching.graph.preview.contract.ProfilePreviewIntent +import com.puzzle.matching.graph.preview.contract.ProfilePreviewSideEffect +import com.puzzle.matching.graph.preview.contract.ProfilePreviewState +import com.puzzle.matching.graph.preview.page.BasicInfoPage +import com.puzzle.matching.graph.preview.page.ValuePickPage +import com.puzzle.matching.graph.preview.page.ValueTalkPage + +@Composable +internal fun ProfilePreviewRoute( + viewModel: ProfilePreviewViewModel = mavericksViewModel(), +) { + val state by viewModel.collectAsState() + + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(viewModel) { + lifecycleOwner.repeatOnStarted { + viewModel.sideEffects.collect { sideEffect -> + when (sideEffect) { + is ProfilePreviewSideEffect.Navigate -> TODO() + } + } + } + } + + ProfilePreviewScreen( + state = state, + onCloseClick = { viewModel.onIntent(ProfilePreviewIntent.OnCloseClick) }, + ) +} + +@Composable +private fun ProfilePreviewScreen( + state: ProfilePreviewState, + onCloseClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var currentPage by remember { mutableStateOf(ProfilePreviewState.Page.BasicInfoPage) } + var showDialog by remember { mutableStateOf(false) } + + BackgroundImage(modifier = modifier) + + if (showDialog) { + PieceImageDialog( + imageUri = state.profile?.imageUrl, + buttonLabel = "매칭 수락하기", + onDismissRequest = { showDialog = false }, + isApproveButtonShow = false, + ) + } + + Box( + modifier = modifier + .fillMaxSize() + .then( + if (currentPage != ProfilePreviewState.Page.BasicInfoPage) { + Modifier.background(PieceTheme.colors.light3) + } else { + Modifier + } + ) + ) { + val topBarHeight = 60.dp + val bottomBarHeight = 74.dp + + ProfilePreviewContent( + state = state, + currentPage = currentPage, + modifier = Modifier + .fillMaxSize() + .padding( + top = topBarHeight, + bottom = bottomBarHeight, + ), + ) + + PieceSubCloseTopBar( + title = currentPage.title, + onCloseClick = onCloseClick, + modifier = Modifier + .fillMaxWidth() + .height(topBarHeight) + .align(Alignment.TopCenter) + .then( + if (currentPage != ProfilePreviewState.Page.BasicInfoPage) { + Modifier.background(PieceTheme.colors.white) + } else { + Modifier + } + ) + .padding(horizontal = 20.dp), + ) + + ProfilePreviewBottomBar( + currentPage = currentPage, + onNextPageClick = { + currentPage.getNextPage()?.let { page -> + currentPage = page + } + }, + onPreviousPageClick = { + currentPage.getPreviousPage()?.let { page -> + currentPage = page + } + }, + onShowPicturesClick = { + showDialog = true + }, + modifier = Modifier + .fillMaxWidth() + .height(bottomBarHeight) + .padding(top = 12.dp, bottom = 10.dp) + .padding(horizontal = 20.dp) + .align(Alignment.BottomCenter), + ) + } +} + + +@Composable +private fun ProfilePreviewContent( + state: ProfilePreviewState, + currentPage: ProfilePreviewState.Page, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier.fillMaxSize()) { + state.profile?.let { profile -> + AnimatedContent( + targetState = currentPage, + transitionSpec = { + fadeIn(tween(700)) togetherWith fadeOut(tween(700)) + }, + modifier = Modifier.fillMaxSize(), + ) { + when (it) { + ProfilePreviewState.Page.BasicInfoPage -> + BasicInfoPage( + nickName = profile.nickname, + selfDescription = profile.description, + birthYear = profile.birthYear, + age = profile.age, + height = profile.height, + weight = profile.weight, + location = profile.location, + job = profile.job, + smokingStatus = profile.smokingStatus, + ) + + ProfilePreviewState.Page.ValueTalkPage -> + ValueTalkPage( + nickName = profile.nickname, + selfDescription = profile.description, + talkCards = profile.valueTalks, + ) + + ProfilePreviewState.Page.ValuePickPage -> + ValuePickPage( + nickName = profile.nickname, + selfDescription = profile.description, + pickCards = profile.valuePicks, + ) + } + } + } ?: PieceLoading() + } +} + +@Composable +private fun ProfilePreviewBottomBar( + currentPage: ProfilePreviewState.Page, + onPreviousPageClick: () -> Unit, + onNextPageClick: () -> Unit, + onShowPicturesClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + if (currentPage == ProfilePreviewState.Page.BasicInfoPage) { + PieceRoundingOutlinedButton( + label = stringResource(R.string.check_photo), + onClick = onShowPicturesClick, + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_profile_image), + contentDescription = "이전 페이지 버튼", + modifier = Modifier + .size(52.dp) + .clickable { + onShowPicturesClick() + }, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + if (currentPage == ProfilePreviewState.Page.BasicInfoPage) { + Image( + painter = painterResource(id = R.drawable.ic_left_disable), + contentDescription = "이전 페이지 버튼", + modifier = Modifier + .size(52.dp), + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_left_able), + contentDescription = "이전 페이지 버튼", + modifier = Modifier + .size(52.dp) + .clickable { + onPreviousPageClick() + }, + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + if (currentPage == ProfilePreviewState.Page.ValuePickPage) { + Image( + painter = painterResource(id = R.drawable.ic_right_disable), + contentDescription = "다음 페이지 버튼", + modifier = Modifier + .size(52.dp) + .clickable { onNextPageClick() }, + ) + } else { + Image( + painter = painterResource(id = R.drawable.ic_right_able), + contentDescription = "다음 페이지 버튼", + modifier = Modifier + .size(52.dp) + .clickable { onNextPageClick() }, + ) + } + } +} + +@Composable +private fun BackgroundImage(modifier: Modifier = Modifier) { + Image( + painter = painterResource(id = R.drawable.matchingdetail_bg), + contentDescription = "basic info 배경화면", + contentScale = ContentScale.Crop, + modifier = modifier.fillMaxSize(), + ) +} + +@Preview +@Composable +private fun ProfilePreviewScreenPreview() { + PieceTheme { + ProfilePreviewScreen( + state = ProfilePreviewState( + profile = OpponentProfile( + description = "음악과 요리를 좋아하는", + nickname = "수줍은 수달", + birthYear = "00", + age = 25, + height = 254, + weight = 72, + job = "개발자", + location = "서울특별시", + smokingStatus = "비흡연", + valueTalks = emptyList(), + valuePicks = emptyList(), + imageUrl = "", + ) + ), + onCloseClick = {}, + ) + } +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/preview/ProfilePreviewViewModel.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/ProfilePreviewViewModel.kt new file mode 100644 index 00000000..f8600dcc --- /dev/null +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/ProfilePreviewViewModel.kt @@ -0,0 +1,61 @@ +package com.puzzle.matching.graph.preview + +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.puzzle.common.event.EventHelper +import com.puzzle.domain.model.error.ErrorHelper +import com.puzzle.matching.graph.preview.contract.ProfilePreviewIntent +import com.puzzle.matching.graph.preview.contract.ProfilePreviewSideEffect +import com.puzzle.matching.graph.preview.contract.ProfilePreviewState +import com.puzzle.navigation.NavigationEvent +import com.puzzle.navigation.NavigationHelper +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +class ProfilePreviewViewModel @AssistedInject constructor( + @Assisted initialState: ProfilePreviewState, + private val navigationHelper: NavigationHelper, + internal val eventHelper: EventHelper, + private val errorHelper: ErrorHelper, +) : MavericksViewModel(initialState) { + private val intents = Channel(BUFFERED) + private val _sideEffects = Channel(BUFFERED) + val sideEffects = _sideEffects.receiveAsFlow() + + init { + intents.receiveAsFlow() + .onEach(::processIntent) + .launchIn(viewModelScope) + } + + internal fun onIntent(intent: ProfilePreviewIntent) = viewModelScope.launch { + intents.send(intent) + } + + private suspend fun processIntent(intent: ProfilePreviewIntent) { + when (intent) { + ProfilePreviewIntent.OnCloseClick -> moveToBackScreen() + } + } + + private suspend fun moveToBackScreen() { + _sideEffects.send(ProfilePreviewSideEffect.Navigate(NavigationEvent.NavigateUp)) + } + + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: ProfilePreviewState): ProfilePreviewViewModel + } + + companion object : + MavericksViewModelFactory by hiltMavericksViewModelFactory() +} diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewIntent.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewIntent.kt new file mode 100644 index 00000000..2259b4ba --- /dev/null +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewIntent.kt @@ -0,0 +1,5 @@ +package com.puzzle.matching.graph.preview.contract + +sealed class ProfilePreviewIntent { + data object OnCloseClick : ProfilePreviewIntent() +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewSideEffect.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewSideEffect.kt new file mode 100644 index 00000000..7d812980 --- /dev/null +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewSideEffect.kt @@ -0,0 +1,7 @@ +package com.puzzle.matching.graph.preview.contract + +import com.puzzle.navigation.NavigationEvent + +sealed class ProfilePreviewSideEffect { + data class Navigate(val navigationEvent: NavigationEvent) : ProfilePreviewSideEffect() +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewState.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewState.kt new file mode 100644 index 00000000..508b351b --- /dev/null +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/contract/ProfilePreviewState.kt @@ -0,0 +1,31 @@ +package com.puzzle.matching.graph.preview.contract + +import com.airbnb.mvrx.MavericksState +import com.puzzle.domain.model.profile.OpponentProfile + +data class ProfilePreviewState( + val isLoading: Boolean = false, + val profile: OpponentProfile? = null, +) : MavericksState { + + enum class Page(val title: String) { + BasicInfoPage(title = ""), + ValueTalkPage(title = "가치관 Talk"), + ValuePickPage(title = "가치관 Pick"), + ; + + fun getNextPage(): Page? = + when (this) { + BasicInfoPage -> ValueTalkPage + ValueTalkPage -> ValuePickPage + ValuePickPage -> null + } + + fun getPreviousPage(): Page? = + when (this) { + BasicInfoPage -> null + ValueTalkPage -> BasicInfoPage + ValuePickPage -> ValueTalkPage + } + } +} diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/BasicInfoPage.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/BasicInfoPage.kt new file mode 100644 index 00000000..e1e51733 --- /dev/null +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/BasicInfoPage.kt @@ -0,0 +1,269 @@ +package com.puzzle.matching.graph.preview.page + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.puzzle.designsystem.R +import com.puzzle.designsystem.foundation.PieceTheme +import com.puzzle.matching.graph.detail.common.component.BasicInfoHeader + +@Composable +internal fun BasicInfoPage( + nickName: String, + selfDescription: String, + birthYear: String, + age: Int, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + BasicInfoName( + nickName = nickName, + selfDescription = selfDescription, + modifier = Modifier + .padding(vertical = 20.dp) + .weight(1f), + ) + + BasicInfoCard( + age = age, + birthYear = birthYear, + height = height, + weight = weight, + location = location, + job = job, + smokingStatus = smokingStatus, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } +} + +@Composable +private fun BasicInfoName( + nickName: String, + selfDescription: String, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.padding(horizontal = 20.dp)) { + Text( + text = stringResource(R.string.basicinfo_main_label), + style = PieceTheme.typography.bodyMM, + color = PieceTheme.colors.primaryDefault, + ) + + Spacer(modifier = Modifier.weight(1f)) + + BasicInfoHeader( + nickName = nickName, + selfDescription = selfDescription, + hasMoreButton = false, + onMoreClick = {}, + ) + } +} + +@Composable +private fun BasicInfoCard( + age: Int, + birthYear: String, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 4.dp) + ) { + InfoItem( + title = stringResource(R.string.basicinfocard_age), + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.basicinfocard_age_particle), + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.black, + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = age.toString(), + style = PieceTheme.typography.headingSSB, + color = PieceTheme.colors.black, + ) + + Text( + text = stringResource(R.string.basicinfocard_age_classifier), + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.black, + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = birthYear + stringResource(R.string.basicinfocard_age_suffix), + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark2, + modifier = Modifier.padding(top = 1.dp), + ) + } + }, + modifier = Modifier.size( + width = 144.dp, + height = 80.dp, + ), + ) + + InfoItem( + title = stringResource(R.string.basicinfocard_height), + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = height.toString(), + style = PieceTheme.typography.headingSSB, + color = PieceTheme.colors.black, + ) + + Text( + text = "cm", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.black, + ) + } + }, + modifier = Modifier.weight(1f), + ) + + InfoItem( + title = "몸무게", + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = weight.toString(), + style = PieceTheme.typography.headingSSB, + color = PieceTheme.colors.black, + ) + + Text( + text = "kg", + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.black, + ) + } + }, + modifier = Modifier.weight(1f), + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier + .fillMaxWidth() + .padding(bottom = 12.dp) + ) { + InfoItem( + title = stringResource(R.string.basicinfocard_location), + content = location, + modifier = Modifier.size( + width = 144.dp, + height = 80.dp, + ), + ) + + InfoItem( + title = stringResource(R.string.basicinfocard_job), + content = job, + modifier = Modifier.weight(1f), + ) + + InfoItem( + title = stringResource(R.string.basicinfocard_smokingStatus), + content = smokingStatus, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun InfoItem( + title: String, + modifier: Modifier = Modifier, + content: String? = null, + backgroundColor: Color = PieceTheme.colors.white, + text: @Composable ColumnScope.() -> Unit? = {}, +) { + Column( + verticalArrangement = Arrangement.spacedBy( + space = 8.dp, + alignment = Alignment.CenterVertically + ), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .height(80.dp) + .clip(RoundedCornerShape(8.dp)) + .background(backgroundColor) + .padding(horizontal = 12.dp), + ) { + Text( + text = title, + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark2, + ) + + if (content != null) { + Text( + text = content, + style = PieceTheme.typography.headingSSB, + color = PieceTheme.colors.black, + ) + } else { + text() + } + } +} + +@Preview +@Composable +private fun ProfileBasicInfoPagePreview() { + PieceTheme { + BasicInfoPage( + nickName = "수줍은 수달", + selfDescription = "음악과 요리를 좋아하는", + birthYear = "1994", + age = 31, + height = 200, + weight = 72, + location = "서울특별시", + job = "개발자", + smokingStatus = "비흡연", + modifier = Modifier.background(PieceTheme.colors.dark1) + ) + } +} diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/ValuePickPage.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/ValuePickPage.kt new file mode 100644 index 00000000..98fff546 --- /dev/null +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/ValuePickPage.kt @@ -0,0 +1,235 @@ +package com.puzzle.matching.graph.preview.page + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.puzzle.common.ui.CollapsingHeaderNestedScrollConnection +import com.puzzle.designsystem.R +import com.puzzle.designsystem.component.PieceSubButton +import com.puzzle.designsystem.foundation.PieceTheme +import com.puzzle.domain.model.profile.AnswerOption +import com.puzzle.domain.model.profile.OpponentValuePick +import com.puzzle.matching.graph.detail.common.component.BasicInfoHeader + +@Composable +internal fun ValuePickPage( + nickName: String, + selfDescription: String, + pickCards: List, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + + val valuePickHeaderHeight = 104.dp + + val valuePickHeaderHeightPx = with(density) { valuePickHeaderHeight.roundToPx() } + + val connection = remember(valuePickHeaderHeightPx) { + CollapsingHeaderNestedScrollConnection(valuePickHeaderHeightPx) + } + + val spaceHeight by remember(density) { + derivedStateOf { + with(density) { + (valuePickHeaderHeightPx + connection.headerOffset).toDp() + } + } + } + + Box(modifier = modifier.nestedScroll(connection)) { + Column { + Spacer(Modifier.height(spaceHeight)) + + ValuePickCards( + pickCards = pickCards, + modifier = Modifier.fillMaxSize(), + ) + } + + BasicInfoHeader( + nickName = nickName, + selfDescription = selfDescription, + onMoreClick = { }, + hasMoreButton = false, + modifier = Modifier + .offset { IntOffset(0, connection.headerOffset) } + .background(PieceTheme.colors.white) + .height(valuePickHeaderHeight) + .padding( + vertical = 20.dp, + horizontal = 20.dp + ), + ) + + Spacer( + modifier = Modifier + .height(1.dp) + .fillMaxWidth() + .background(PieceTheme.colors.light2) + .align(Alignment.TopCenter), + ) + } +} + +@Composable +private fun ValuePickCards( + pickCards: List, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(PieceTheme.colors.light3) + .padding(horizontal = 20.dp), + ) { + itemsIndexed(pickCards) { idx, item -> + ValuePickCard( + valuePickQuestion = item, + modifier = Modifier.padding(top = 20.dp) + ) + } + + item { + Spacer(modifier = Modifier.height(60.dp)) + } + } +} + +@Composable +private fun ValuePickCard( + valuePickQuestion: OpponentValuePick, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(PieceTheme.colors.white) + .padding( + horizontal = 20.dp, + vertical = 24.dp, + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(28.dp), + ) { + Image( + painter = painterResource(id = R.drawable.ic_question), + contentDescription = "basic info 배경화면", + modifier = Modifier + .size(20.dp), + ) + + Spacer(modifier = Modifier.width(6.dp)) + + Text( + text = valuePickQuestion.category, + style = PieceTheme.typography.bodySSB, + color = PieceTheme.colors.primaryDefault, + ) + } + + Text( + text = valuePickQuestion.question, + style = PieceTheme.typography.headingMSB, + color = PieceTheme.colors.dark1, + modifier = Modifier.padding(top = 12.dp, bottom = 24.dp), + ) + + valuePickQuestion.answerOptions.forEachIndexed { idx, answer -> + PieceSubButton( + label = answer.content, + onClick = {}, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .then( + if (idx != 0) Modifier.padding(top = 8.dp) + else Modifier + ), + ) + } + } +} + +@Preview +@Composable +private fun ProfileValuePickPagePreview() { + PieceTheme { + ValuePickPage( + nickName = "nickName", + selfDescription = "selfDescription", + pickCards = listOf( + OpponentValuePick( + category = "음주", + question = "사귀는 사람과 함께 술을 마시는 것을 좋아하나요?", + answerOptions = listOf( + AnswerOption(1, "함께 술을 즐기고 싶어요"), + AnswerOption(2, "같이 술을 즐길 수 없어도 괜찮아요") + ), + selectedAnswer = 1, + isSameWithMe = true, + ), + OpponentValuePick( + category = "만남 빈도", + question = "주말에 얼마나 자주 데이트를 하고싶나요?", + answerOptions = listOf( + AnswerOption(1, "주말에는 최대한 같이 있고 싶어요"), + AnswerOption(2, "하루 정도는 각자 보내고 싶어요") + ), + selectedAnswer = 1, + isSameWithMe = false, + ), + OpponentValuePick( + category = "연락 빈도", + question = "연인 사이에 얼마나 자주 연락하는게 좋은가요?", + answerOptions = listOf( + AnswerOption(1, "바빠도 최대한 자주 연락하고 싶어요"), + AnswerOption(2, "연락은 생각날 때만 종종 해도 괜찮아요") + ), + selectedAnswer = 1, + isSameWithMe = true, + ), + OpponentValuePick( + category = "연락 방식", + question = "연락할 때 어떤 방법을 더 좋아하나요?", + answerOptions = listOf( + AnswerOption(1, "전화보다는 문자나 카톡이 좋아요"), + AnswerOption(2, "문자나 카톡보다는 전화가 좋아요") + ), + selectedAnswer = 1, + isSameWithMe = false, + ), + ), + ) + } +} diff --git a/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/ValueTalkPage.kt b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/ValueTalkPage.kt new file mode 100644 index 00000000..65cbe6d7 --- /dev/null +++ b/feature/matching/src/main/java/com/puzzle/matching/graph/preview/page/ValueTalkPage.kt @@ -0,0 +1,212 @@ +package com.puzzle.matching.graph.preview.page + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.puzzle.common.ui.CollapsingHeaderNestedScrollConnection +import com.puzzle.designsystem.R +import com.puzzle.designsystem.foundation.PieceTheme +import com.puzzle.domain.model.profile.OpponentValueTalk +import com.puzzle.matching.graph.detail.common.component.BasicInfoHeader + +@Composable +internal fun ValueTalkPage( + nickName: String, + selfDescription: String, + talkCards: List, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + // 1) 고정 헤더 높이(105.dp) + val valueTalkHeaderHeight = 104.dp + val valueTalkHeaderHeightPx = with(density) { valueTalkHeaderHeight.roundToPx() } + + // 2) 헤더가 얼마나 접혔는지(offset)를 관리해주는 NestedScrollConnection + val connection = remember(valueTalkHeaderHeightPx) { + CollapsingHeaderNestedScrollConnection(valueTalkHeaderHeightPx) + } + + // 3) 헤더와 리스트 간 공간(Spacer)에 사용할 height (DP) + // 헤더가 접힐수록 headerOffset이 음수가 되면서 spaceHeight가 줄어듦 + val spaceHeight by remember(density) { + derivedStateOf { + with(density) { + // (헤더 높이 + offset)를 DP로 변환 + // offset이 -105px이면 0dp, + // offset이 0px이면 105dp + (valueTalkHeaderHeightPx + connection.headerOffset).toDp() + } + } + } + + // 4) Box에 nestedScroll(connection)을 달아, 스크롤 이벤트가 + // CollapsingHeaderNestedScrollConnection으로 전달되도록 함 + Box(modifier = modifier.nestedScroll(connection)) { + // 5) Column: Spacer + LazyColumn을 세로로 배치 + // 헤더가 접힐수록 Spacer의 높이가 줄어들고, 그만큼 리스트가 위로 올라옴 + Column { + // 5-1) 헤더 높이만큼 Spacer를 줘서 리스트가 '헤더 아래'에서 시작 + // 헤더 offset이 변하면, spaceHeight가 변해 리스트도 따라 위로 올라감 + ValueTalkCards( + talkCards = talkCards, + modifier = Modifier.padding(top = spaceHeight), + ) + } + + // 6) 실제 헤더 뷰 + // offset을 통해 y축 이동 (headerOffset이 음수면 위로 올라가며 사라짐) + BasicInfoHeader( + nickName = nickName, + selfDescription = selfDescription, + hasMoreButton = false, + modifier = Modifier + .offset { IntOffset(0, connection.headerOffset) } + .background(PieceTheme.colors.white) + .height(valueTalkHeaderHeight) + .padding(vertical = 20.dp, horizontal = 20.dp), + ) + + HorizontalDivider( + thickness = 1.dp, + color = PieceTheme.colors.light2, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopCenter), + ) + } +} + +@Composable +private fun ValueTalkCards( + talkCards: List, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(PieceTheme.colors.light3) + .padding(horizontal = 20.dp), + ) { + itemsIndexed(talkCards) { idx, item -> + ValueTalkCard( + item = item, + idx = idx, + modifier = Modifier.padding(top = 20.dp), + ) + } + + item { + Spacer(Modifier.height(60.dp)) + } + } +} + +@Composable +private fun ValueTalkCard( + item: OpponentValueTalk, + idx: Int, + modifier: Modifier = Modifier, +) { + val icons = remember { + listOf( + R.drawable.ic_puzzle1, + R.drawable.ic_puzzle2, + R.drawable.ic_puzzle3, + ) + } + + val idxInRange = idx % icons.size + + Column( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(PieceTheme.colors.white) + .padding( + top = 20.dp, + bottom = 32.dp, + start = 20.dp, + end = 20.dp, + ), + ) { + Text( + text = item.category, + style = PieceTheme.typography.bodyMM, + color = PieceTheme.colors.primaryDefault, + modifier = Modifier.padding(bottom = 24.dp), + ) + + Image( + painter = painterResource(id = icons[idxInRange]), + contentDescription = "basic info 배경화면", + modifier = Modifier.size(60.dp), + ) + + Text( + text = item.summary, + style = PieceTheme.typography.headingMSB, + color = PieceTheme.colors.black, + modifier = Modifier.padding(top = 24.dp), + ) + + Text( + text = item.answer, + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.dark2, + modifier = Modifier.padding(top = 8.dp), + ) + } +} + +@Preview +@Composable +private fun ProfileValueTalkPagePreview() { + PieceTheme { + ValueTalkPage( + nickName = "수줍은 수달", + selfDescription = "음악과 요리를 좋아하는", + talkCards = listOf( + OpponentValueTalk( + category = "꿈과 목표", + summary = "여행하며 문화 경험, LGBTQ+ 변화를 원해요.", + answer = "안녕하세요! 저는 삶의 매 순간을 소중히 여기며, 꿈과 목표를 이루기 위해 노력하는 사람입니다. 제 가장 큰 꿈은 여행을 통해 다양한 문화와 사람들을 경험하고, 그 과정에서 얻은 지혜를 나누는 것입니다. 또한, LGBTQ+ 커뮤니티를 위한 긍정적인 변화를 이끌어내고 싶습니다. 내가 이루고자 하는 목표는 나 자신을 발전시키고, 사랑하는 사람들과 함께 행복한 순간들을 만드는 것입니다. 서로의 꿈을 지지하며 함께 성장할 수 있는 관계를 기대합니다!", + ), + OpponentValueTalk( + category = "관심사와 취향", + summary = "음악, 요리, 하이킹을 좋아해요.", + answer = "저는 다양한 취미와 관심사를 가진 사람입니다. 음악을 사랑하여 콘서트에 자주 가고, 특히 인디 음악과 재즈에 매력을 느낍니다. 요리도 좋아해 새로운 레시피에 도전하는 것을 즐깁니다. 여행을 통해 새로운 맛과 문화를 경험하는 것도 큰 기쁨입니다. 또, 자연을 사랑해서 주말마다 하이킹이나 캠핑을 자주 떠납니다. 영화와 책도 좋아해, 좋은 이야기와 감동을 나누는 시간을 소중히 여깁니다. 서로의 취향을 공유하며 즐거운 시간을 보낼 수 있기를 기대합니다!", + ), + OpponentValueTalk( + category = "연애관", + summary = "서로 존중하고 신뢰하며, 함께 성장하는 관계를 원해요. ", + answer = "저는 연애에서 서로의 존중과 신뢰가 가장 중요하다고 생각합니다. 진정한 소통을 통해 서로의 감정을 이해하고, 함께 성장할 수 있는 관계를 원합니다. 일상 속 작은 것에도 감사하며, 서로의 꿈과 목표를 지지하고 응원하는 파트너가 되고 싶습니다. 또한, 유머와 즐거움을 잃지 않으며, 함께하는 순간들을 소중히 여기고 싶습니다. 사랑은 서로를 더 나은 사람으로 만들어주는 힘이 있다고 믿습니다. 서로에게 긍정적인 영향을 주며 행복한 시간을 함께하고 싶습니다!" + ) + ) + ) + } +} diff --git a/feature/matching/src/main/java/com/puzzle/matching/navigation/MatchingNavigation.kt b/feature/matching/src/main/java/com/puzzle/matching/navigation/MatchingNavigation.kt index 354bcc2f..1396c14d 100644 --- a/feature/matching/src/main/java/com/puzzle/matching/navigation/MatchingNavigation.kt +++ b/feature/matching/src/main/java/com/puzzle/matching/navigation/MatchingNavigation.kt @@ -8,6 +8,7 @@ import com.puzzle.matching.graph.block.BlockRoute import com.puzzle.matching.graph.contact.ContactRoute import com.puzzle.matching.graph.detail.MatchingDetailRoute import com.puzzle.matching.graph.main.MatchingRoute +import com.puzzle.matching.graph.preview.ProfilePreviewRoute import com.puzzle.matching.graph.report.ReportRoute import com.puzzle.navigation.MatchingGraph import com.puzzle.navigation.MatchingGraphDest @@ -40,8 +41,12 @@ fun NavGraphBuilder.matchingNavGraph() { ) } - composable { backStackEntry -> + composable { ContactRoute() } + + composable { + ProfilePreviewRoute() + } } } diff --git a/feature/onboarding/src/main/java/com/puzzle/onboarding/OnboardingScreen.kt b/feature/onboarding/src/main/java/com/puzzle/onboarding/OnboardingScreen.kt index 5a82a733..a436bb5e 100644 --- a/feature/onboarding/src/main/java/com/puzzle/onboarding/OnboardingScreen.kt +++ b/feature/onboarding/src/main/java/com/puzzle/onboarding/OnboardingScreen.kt @@ -40,7 +40,6 @@ import com.puzzle.designsystem.component.PieceSolidButton import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.navigation.AuthGraphDest import com.puzzle.navigation.NavigationEvent -import com.puzzle.navigation.OnboardingRoute import com.puzzle.onboarding.contract.OnboardingIntent import com.puzzle.onboarding.contract.OnboardingSideEffect import kotlinx.coroutines.launch @@ -103,7 +102,7 @@ internal fun OnboardingScreen(onStartButtonClick: () -> Unit) { Column(modifier = Modifier.fillMaxSize()) { when (page) { 0 -> OnboardingPageContent( - imageRes = R.drawable.ic_onboarding_camera, + imageRes = R.drawable.ic_onboarding_matching, title = stringResource(R.string.one_day_one_matching_title), description = stringResource(R.string.one_day_one_matching_description), ) diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/basic/BasicProfileScreen.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/basic/BasicProfileScreen.kt index 03c21126..7aff548f 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/basic/BasicProfileScreen.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/basic/BasicProfileScreen.kt @@ -1,5 +1,9 @@ package com.puzzle.profile.graph.basic +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -14,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,7 +29,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -37,9 +44,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.text.isDigitsOnly import androidx.lifecycle.compose.LocalLifecycleOwner +import coil3.compose.AsyncImage import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import com.puzzle.common.ui.repeatOnStarted +import com.puzzle.common.ui.throttledClickable import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceChip import com.puzzle.designsystem.component.PieceSolidButton @@ -49,7 +58,7 @@ import com.puzzle.designsystem.component.PieceTextInputDropDown import com.puzzle.designsystem.component.PieceTextInputSnsDropDown import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.domain.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType import com.puzzle.profile.graph.basic.contract.BasicProfileIntent import com.puzzle.profile.graph.basic.contract.BasicProfileSideEffect import com.puzzle.profile.graph.basic.contract.BasicProfileState @@ -83,18 +92,19 @@ internal fun BasicProfileRoute( onSaveClick = { viewModel.onIntent(BasicProfileIntent.SaveBasicProfile) }, onBackClick = { viewModel.onIntent(BasicProfileIntent.OnBackClick) }, onNickNameChanged = { viewModel.onIntent(BasicProfileIntent.UpdateNickName(it)) }, + onProfileImageChanged = { viewModel.onIntent(BasicProfileIntent.UpdateProfileImage(it)) }, onDuplicationCheckClick = { viewModel.onIntent(BasicProfileIntent.CheckNickNameDuplication) }, onDescribeMySelfChanged = { viewModel.onIntent(BasicProfileIntent.UpdateDescribeMySelf(it)) }, onBirthdayChanged = { viewModel.onIntent(BasicProfileIntent.UpdateBirthday(it)) }, onHeightChanged = { viewModel.onIntent(BasicProfileIntent.UpdateHeight(it)) }, onWeightChanged = { viewModel.onIntent(BasicProfileIntent.UpdateWeight(it)) }, - onSmokeStatusChanged = { viewModel.onIntent(BasicProfileIntent.UpdateSmokeStatus(it)) }, + onSmokingStatusChanged = { viewModel.onIntent(BasicProfileIntent.UpdateSmokingStatus(it)) }, onSnsActivityChanged = { viewModel.onIntent(BasicProfileIntent.UpdateSnsActivity(it)) }, onAddContactClick = { viewModel.onIntent( BasicProfileIntent.ShowBottomSheet { ContactBottomSheet( - usingSnsPlatform = state.usingSnsPlatforms, + usingContactType = state.usingSnsPlatforms, isEdit = false, onButtonClicked = { viewModel.onIntent(BasicProfileIntent.AddContact(it)) @@ -107,13 +117,13 @@ internal fun BasicProfileRoute( viewModel.onIntent( BasicProfileIntent.ShowBottomSheet { ContactBottomSheet( - usingSnsPlatform = state.usingSnsPlatforms, - nowSnsPlatform = state.contacts[idx].snsPlatform, + usingContactType = state.usingSnsPlatforms, + nowContactType = state.contacts[idx].type, isEdit = true, onButtonClicked = { viewModel.onIntent( BasicProfileIntent.UpdateContact( - idx, state.contacts[idx].copy(snsPlatform = it) + idx, state.contacts[idx].copy(type = it) ) ) @@ -162,6 +172,7 @@ private fun BasicProfileScreen( onSaveClick: () -> Unit, onBackClick: () -> Unit, onNickNameChanged: (String) -> Unit, + onProfileImageChanged: (String) -> Unit, onDuplicationCheckClick: () -> Unit, onDescribeMySelfChanged: (String) -> Unit, onBirthdayChanged: (String) -> Unit, @@ -169,7 +180,7 @@ private fun BasicProfileScreen( onHeightChanged: (String) -> Unit, onWeightChanged: (String) -> Unit, onJobDropDownClicked: () -> Unit, - onSmokeStatusChanged: (Boolean) -> Unit, + onSmokingStatusChanged: (Boolean) -> Unit, onSnsActivityChanged: (Boolean) -> Unit, onContactChange: (Int, Contact) -> Unit, onSnsPlatformChange: (Int) -> Unit, @@ -213,8 +224,9 @@ private fun BasicProfileScreen( ) PhotoContent( - onEditPhotoClick = {}, - screenState = state.profileScreenState, + imageUrl = state.imageUrl, + imageUrlInputState = state.imageUrlInputState, + onProfileImageChanged = onProfileImageChanged, modifier = Modifier .align(Alignment.CenterHorizontally) .padding(top = 40.dp) @@ -222,7 +234,7 @@ private fun BasicProfileScreen( ) NickNameContent( - nickName = state.nickName, + nickName = state.nickname, nickNameGuideMessage = state.nickNameGuideMessage, isCheckingButtonAvailable = state.isCheckingButtonEnabled, onNickNameChanged = onNickNameChanged, @@ -288,7 +300,7 @@ private fun BasicProfileScreen( SmokeContent( isSmoke = state.isSmoke, - onSmokeStatusChanged = onSmokeStatusChanged, + onSmokingStatusChanged = onSmokingStatusChanged, modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), @@ -332,11 +344,11 @@ private fun ColumnScope.SnsPlatformContent( SectionTitle(title = "연락처") contacts.forEachIndexed { idx, contact -> - val image = when (contact.snsPlatform) { - SnsPlatform.KAKAO_TALK_ID -> R.drawable.ic_sns_kakao - SnsPlatform.OPEN_CHAT_URL -> R.drawable.ic_sns_openchatting - SnsPlatform.INSTAGRAM_ID -> R.drawable.ic_sns_instagram - SnsPlatform.PHONE_NUMBER -> R.drawable.ic_sns_call + val image = when (contact.type) { + ContactType.KAKAO_TALK_ID -> R.drawable.ic_sns_kakao + ContactType.OPEN_CHAT_URL -> R.drawable.ic_sns_openchatting + ContactType.INSTAGRAM_ID -> R.drawable.ic_sns_instagram + ContactType.PHONE_NUMBER -> R.drawable.ic_sns_call else -> R.drawable.ic_delete_circle // 임시 } @@ -421,7 +433,7 @@ private fun SnsActivityContent( @Composable private fun SmokeContent( isSmoke: Boolean, - onSmokeStatusChanged: (Boolean) -> Unit, + onSmokingStatusChanged: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { SectionTitle(title = "흡연") @@ -433,14 +445,14 @@ private fun SmokeContent( PieceChip( label = "흡연", selected = isSmoke, - onChipClicked = { onSmokeStatusChanged(true) }, + onChipClicked = { onSmokingStatusChanged(true) }, modifier = Modifier.weight(1f), ) PieceChip( label = "비흡연", selected = !isSmoke, - onChipClicked = { onSmokeStatusChanged(false) }, + onChipClicked = { onSmokingStatusChanged(false) }, modifier = Modifier.weight(1f), ) } @@ -679,8 +691,7 @@ private fun SelfDescriptionContent( onDescribeMySelfChanged: (String) -> Unit, modifier: Modifier = Modifier, ) { - val isSaveFailed: Boolean = - descriptionInputState == InputState.WARNIING + val isSaveFailed: Boolean = descriptionInputState == InputState.WARNIING var isInputFocused by remember { mutableStateOf(false) } val isGuidanceVisibe: Boolean = isInputFocused || description.isNotBlank() || isSaveFailed @@ -843,32 +854,71 @@ private fun NickNameContent( @Composable private fun PhotoContent( - onEditPhotoClick: () -> Unit, - screenState: ScreenState, + imageUrl: String, + imageUrlInputState: InputState, + onProfileImageChanged: (String) -> Unit, modifier: Modifier = Modifier, ) { - // TODO : 이미지 조건 추가, screenState == BasicProfileState.ScreenState.SAVE_FAILED && 이미지가 없을 경우 - val isSaveFailed: Boolean = screenState == ScreenState.SAVE_FAILED + val isSaveFailed: Boolean = imageUrlInputState == InputState.WARNIING && imageUrl.isEmpty() - Box(modifier = modifier) { - Image( - painter = painterResource(R.drawable.ic_profile_default), - contentDescription = null, - ) + val singlePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> + if (uri != null) { + onProfileImageChanged(uri.toString()) + } + } + ) - Image( - painter = if (isSaveFailed) { - painterResource(R.drawable.ic_photo_error) - } else { - painterResource(R.drawable.ic_edit) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier.throttledClickable(throttleTime = 2000L) { + singlePhotoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) }, - contentDescription = null, - modifier = Modifier - .align(Alignment.BottomEnd) - .clickable { - onEditPhotoClick() - } - ) + ) { + AsyncImage( + model = imageUrl.ifEmpty { R.drawable.ic_profile_default }, + placeholder = painterResource(R.drawable.ic_profile_default), + contentScale = ContentScale.FillBounds, + onError = { error -> + Log.e( + "RegisterProfileScreen", error.result.throwable.stackTraceToString() + ) + }, + contentDescription = null, + modifier = Modifier + .size(120.dp) + .clip(CircleShape), + ) + + Image( + painter = if (isSaveFailed) { + painterResource(R.drawable.ic_photo_error) + } else { + painterResource(R.drawable.ic_edit) + }, + contentDescription = null, + modifier = Modifier.align(Alignment.BottomEnd) + ) + } + + AnimatedVisibility( + visible = isSaveFailed + ) { + Text( + text = stringResource(R.string.basic_profile_required_field), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = PieceTheme.typography.bodySM, + color = PieceTheme.colors.error, + modifier = Modifier.padding(top = 8.dp), + ) + } } } @@ -898,12 +948,13 @@ private fun BasicProfilePreview() { onHeightChanged = {}, onWeightChanged = {}, onJobDropDownClicked = {}, - onSmokeStatusChanged = {}, + onSmokingStatusChanged = {}, onSnsPlatformChange = {}, onContactChange = { _, _ -> }, onSnsActivityChanged = {}, onDeleteClick = {}, onAddContactClick = {}, + onProfileImageChanged = {}, ) } -} \ No newline at end of file +} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/basic/BasicProfileViewModel.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/basic/BasicProfileViewModel.kt index cf90dd71..a255f781 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/basic/BasicProfileViewModel.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/basic/BasicProfileViewModel.kt @@ -7,8 +7,10 @@ import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.puzzle.common.event.EventHelper import com.puzzle.common.event.PieceEvent +import com.puzzle.domain.model.error.ErrorHelper import com.puzzle.domain.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType +import com.puzzle.domain.repository.ProfileRepository import com.puzzle.navigation.NavigationEvent import com.puzzle.navigation.NavigationHelper import com.puzzle.profile.graph.basic.contract.BasicProfileIntent @@ -30,8 +32,10 @@ import kotlinx.coroutines.launch class BasicProfileViewModel @AssistedInject constructor( @Assisted initialState: BasicProfileState, + private val profileRepository: ProfileRepository, internal val navigationHelper: NavigationHelper, private val eventHelper: EventHelper, + private val errorHelper: ErrorHelper, ) : MavericksViewModel(initialState) { private val intents = Channel(BUFFERED) @@ -40,11 +44,32 @@ class BasicProfileViewModel @AssistedInject constructor( private val initialState: BasicProfileState = BasicProfileState() init { + initMyProfile() + intents.receiveAsFlow() .onEach(::processIntent) .launchIn(viewModelScope) + } - // TODO : initialState 초기화 + private fun initMyProfile() = viewModelScope.launch { + profileRepository.retrieveMyProfileBasic() + .onSuccess { + setState { + copy( + description = it.description, + nickname = it.nickname, + birthdate = it.birthDate, + height = it.height.toString(), + weight = it.weight.toString(), + location = it.location, + job = it.job, + isSmoke = it.isSmoke(), + isSnsActive = it.isSnsActive(), + imageUrl = it.imageUrl, + contacts = it.contacts, + ) + } + }.onFailure { errorHelper.sendError(it) } } internal fun onIntent(intent: BasicProfileIntent) = viewModelScope.launch { @@ -63,13 +88,14 @@ class BasicProfileViewModel @AssistedInject constructor( is BasicProfileIntent.UpdateWeight -> updateWeight(intent.weight) is BasicProfileIntent.UpdateJob -> updateJob(intent.job) is BasicProfileIntent.UpdateRegion -> updateLocation(intent.region) - is BasicProfileIntent.UpdateSmokeStatus -> updateIsSmoke(intent.isSmoke) + is BasicProfileIntent.UpdateSmokingStatus -> updateIsSmoke(intent.isSmoke) is BasicProfileIntent.UpdateSnsActivity -> updateIsSnsActive(intent.isSnsActivity) - is BasicProfileIntent.AddContact -> addContact(intent.snsPlatform) + is BasicProfileIntent.AddContact -> addContact(intent.contactType) is BasicProfileIntent.DeleteContact -> deleteContact(intent.idx) is BasicProfileIntent.UpdateContact -> updateContact(intent.idx, intent.contact) is BasicProfileIntent.ShowBottomSheet -> showBottomSheet(intent.content) BasicProfileIntent.HideBottomSheet -> hideBottomSheet() + is BasicProfileIntent.UpdateProfileImage -> updateProfileImage(intent.imageUrl) } } @@ -81,59 +107,77 @@ class BasicProfileViewModel @AssistedInject constructor( private fun saveBasicProfile() { withState { state -> - // 프로필이 미완성일 때 - if (state.isProfileIncomplete) { - setState { - copy( - profileScreenState = ScreenState.SAVE_FAILED, - nickNameGuideMessage = nickNameStateInSavingProfile, - descriptionInputState = getInputState(state.description), - birthdateInputState = getInputState(state.birthdate), - locationInputState = getInputState(state.location), - heightInputState = getInputState(state.height), - weightInputState = getInputState(state.weight), - jobInputState = getInputState(state.job) - ) + viewModelScope.launch { + if (state.isProfileIncomplete) { + setState { + copy( + profileScreenState = ScreenState.SAVE_FAILED, + nickNameGuideMessage = nickNameStateInSavingProfile, + descriptionInputState = getInputState(state.description), + imageUrlInputState = getInputState(state.imageUrl), + birthdateInputState = getInputState(state.birthdate), + locationInputState = getInputState(state.location), + heightInputState = getInputState(state.height), + weightInputState = getInputState(state.weight), + jobInputState = getInputState(state.job) + ) + } + return@launch } - return@withState - } - // 닉네임이 중복 검사를 통과한 상태, 저장 API 호출 진행 - // TODO: 실제 API 호출 후 결과에 따라 isSuccess 값을 갱신하세요. - val isSuccess = true - val updatedScreenState = if (isSuccess) { - ScreenState.SAVED - } else { - ScreenState.SAVE_FAILED - } - setState { - copy( - profileScreenState = updatedScreenState, - nickNameGuideMessage = NickNameGuideMessage.LENGTH_GUIDE, - ) + profileRepository.checkNickname(state.nickname) + .onSuccess { result -> if (!result) return@launch } + .onFailure { + errorHelper.sendError(it) + return@launch + } + + profileRepository.updateMyProfileBasic( + description = state.description, + nickname = state.nickname, + birthDate = state.birthdate, + height = state.height.toInt(), + weight = state.weight.toInt(), + location = state.location, + job = state.job, + smokingStatus = if (state.isSmoke) "흡연" else "비흡연", + snsActivityLevel = if (state.isSnsActive) "활동" else "은둔", + imageUrl = state.imageUrl, + contacts = state.contacts, + ).onSuccess { + setState { + copy(profileScreenState = ScreenState.SAVED) + } + }.onFailure { + setState { copy(profileScreenState = ScreenState.SAVE_FAILED) } + errorHelper.sendError(it) + } } } } - private fun checkNickNameDuplication() { - setState { - // TODO: 실제 API 응답 처리 - val isSuccess = true - copy( - isCheckingButtonEnabled = !isSuccess, - nickNameGuideMessage = if (isSuccess) { - NickNameGuideMessage.AVAILABLE - } else { - NickNameGuideMessage.ALREADY_IN_USE - } - ) + private fun checkNickNameDuplication() = withState { state -> + viewModelScope.launch { + profileRepository.checkNickname(state.nickname) + .onSuccess { result -> + setState { + copy( + isCheckingButtonEnabled = !result, + nickNameGuideMessage = if (result) { + NickNameGuideMessage.AVAILABLE + } else { + NickNameGuideMessage.ALREADY_IN_USE + } + ) + } + }.onFailure { errorHelper.sendError(it) } } } private fun updateNickName(nickName: String) { setState { val newState = copy( - nickName = nickName, + nickname = nickName, nickNameGuideMessage = if (nickName.length > 6) { NickNameGuideMessage.LENGTH_EXCEEDED_ERROR } else { @@ -307,10 +351,10 @@ class BasicProfileViewModel @AssistedInject constructor( } } - private fun addContact(snsPlatform: SnsPlatform) { + private fun addContact(contactType: ContactType) { setState { val newContacts = contacts.toMutableList().apply { - add(Contact(snsPlatform = snsPlatform, content = "")) + add(Contact(type = contactType, content = "")) } val newState = copy( contacts = newContacts, @@ -378,6 +422,15 @@ class BasicProfileViewModel @AssistedInject constructor( } } + private fun updateProfileImage(url: String) { + setState { + copy( + imageUrl = url, + imageUrlInputState = InputState.DEFAULT, + ) + } + } + private fun showBottomSheet(content: @Composable () -> Unit) { eventHelper.sendEvent(PieceEvent.ShowBottomSheet(content)) } @@ -394,4 +447,4 @@ class BasicProfileViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() -} \ No newline at end of file +} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/basic/contract/BasicProfileIntent.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/basic/contract/BasicProfileIntent.kt index 166fc780..3f67c5e7 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/basic/contract/BasicProfileIntent.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/basic/contract/BasicProfileIntent.kt @@ -2,24 +2,25 @@ package com.puzzle.profile.graph.basic.contract import androidx.compose.runtime.Composable import com.puzzle.domain.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType sealed class BasicProfileIntent { data object OnBackClick : BasicProfileIntent() data object SaveBasicProfile : BasicProfileIntent() data class UpdateNickName(val nickName: String) : BasicProfileIntent() data object CheckNickNameDuplication : BasicProfileIntent() + data class UpdateProfileImage(val imageUrl: String) : BasicProfileIntent() data class UpdateDescribeMySelf(val description: String) : BasicProfileIntent() data class UpdateBirthday(val birthday: String) : BasicProfileIntent() data class UpdateHeight(val height: String) : BasicProfileIntent() data class UpdateWeight(val weight: String) : BasicProfileIntent() data class UpdateJob(val job: String) : BasicProfileIntent() data class UpdateRegion(val region: String) : BasicProfileIntent() - data class UpdateSmokeStatus(val isSmoke: Boolean) : BasicProfileIntent() + data class UpdateSmokingStatus(val isSmoke: Boolean) : BasicProfileIntent() data class UpdateSnsActivity(val isSnsActivity: Boolean) : BasicProfileIntent() data class UpdateContact(val idx: Int, val contact: Contact) : BasicProfileIntent() data class DeleteContact(val idx: Int) : BasicProfileIntent() - data class AddContact(val snsPlatform: SnsPlatform) : BasicProfileIntent() + data class AddContact(val contactType: ContactType) : BasicProfileIntent() data class ShowBottomSheet(val content: @Composable () -> Unit) : BasicProfileIntent() data object HideBottomSheet : BasicProfileIntent() -} \ No newline at end of file +} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/basic/contract/BasicProfileState.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/basic/contract/BasicProfileState.kt index 442ec0cc..69e6e7c5 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/basic/contract/BasicProfileState.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/basic/contract/BasicProfileState.kt @@ -3,47 +3,31 @@ package com.puzzle.profile.graph.basic.contract import com.airbnb.mvrx.MavericksState import com.puzzle.designsystem.R import com.puzzle.domain.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform data class BasicProfileState( val profileScreenState: ScreenState = ScreenState.SAVED, - val nickName: String = "수줍은 수달", + val nickname: String = "", val isCheckingButtonEnabled: Boolean = false, val nickNameGuideMessage: NickNameGuideMessage = NickNameGuideMessage.LENGTH_GUIDE, - val description: String = "요리와 음악을 좋아하는", + val description: String = "", val descriptionInputState: InputState = InputState.DEFAULT, - val birthdate: String = "19990909", + val birthdate: String = "", val birthdateInputState: InputState = InputState.DEFAULT, - val location: String = "서울특별시", + val imageUrl: String = "", + val imageUrlInputState: InputState = InputState.DEFAULT, + val location: String = "", val locationInputState: InputState = InputState.DEFAULT, - val height: String = "180", + val height: String = "", val heightInputState: InputState = InputState.DEFAULT, - val weight: String = "72", + val weight: String = "", val weightInputState: InputState = InputState.DEFAULT, - val job: String = "프리랜서", + val job: String = "", val jobInputState: InputState = InputState.DEFAULT, val isSmoke: Boolean = false, val isSnsActive: Boolean = false, - val contacts: List = listOf( - Contact( - snsPlatform = SnsPlatform.KAKAO_TALK_ID, - content = "puzzle1234", - ), - Contact( - snsPlatform = SnsPlatform.INSTAGRAM_ID, - content = "puzzle1234", - ), - Contact( - snsPlatform = SnsPlatform.PHONE_NUMBER, - content = "010-0000-0000", - ), - Contact( - snsPlatform = SnsPlatform.OPEN_CHAT_URL, - content = "https://open.kakao.com/o/s5aqIX1g", - ), - ), + val contacts: List = emptyList(), ) : MavericksState { - val usingSnsPlatforms = contacts.map { it.snsPlatform } + val usingSnsPlatforms = contacts.map { it.type } .toSet() val isProfileIncomplete: Boolean = contacts.isEmpty() || @@ -53,16 +37,17 @@ data class BasicProfileState( height.isBlank() || weight.isBlank() || job.isBlank() || - nickName.isBlank() || + nickname.isBlank() || + imageUrl.isBlank() || nickNameGuideMessage != NickNameGuideMessage.AVAILABLE val nickNameStateInSavingProfile: NickNameGuideMessage = when { - nickName.isBlank() -> { + nickname.isBlank() -> { NickNameGuideMessage.REQUIRED_FIELD } - nickName.length in 1..6 && + nickname.length in 1..6 && nickNameGuideMessage != NickNameGuideMessage.AVAILABLE -> { NickNameGuideMessage.DUPLICATE_CHECK_REQUIRED } diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/main/MainProfileScreen.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/main/MainProfileScreen.kt index 85d9a1c5..d2ef2adf 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/main/MainProfileScreen.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/main/MainProfileScreen.kt @@ -98,9 +98,9 @@ private fun MainProfileScreen( age = state.age, birthYear = state.birthYear, height = state.height, - activityRegion = state.activityRegion, - occupation = state.occupation, - smokeStatue = state.smokeStatue, + location = state.location, + job = state.job, + smokingStatus = state.smokingStatus, weight = state.weight, onBasicProfileClick = onBasicProfileClick, modifier = Modifier.padding(horizontal = 20.dp) @@ -123,13 +123,13 @@ private fun MainProfileScreen( private fun BasicProfile( nickName: String, selfDescription: String, - age: String, + age: Int, birthYear: String, - height: String, - activityRegion: String, - occupation: String, - smokeStatue: String, - weight: String, + height: Int, + location: String, + job: String, + smokingStatus: String, + weight: Int, onBasicProfileClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -178,9 +178,9 @@ private fun BasicProfile( age = age, birthYear = birthYear, height = height, - activityRegion = activityRegion, - occupation = occupation, - smokeStatue = smokeStatue, + location = location, + job = job, + smokingStatus = smokingStatus, weight = weight, modifier = modifier, ) @@ -275,13 +275,13 @@ private fun MyMatchingPieceDetail( @Composable private fun BasicInfoCard( - age: String, + age: Int, birthYear: String, - height: String, - weight: String, - activityRegion: String, - occupation: String, - smokeStatue: String, + height: Int, + weight: Int, + location: String, + job: String, + smokingStatus: String, modifier: Modifier = Modifier, ) { Row( @@ -303,7 +303,7 @@ private fun BasicInfoCard( Spacer(modifier = Modifier.width(4.dp)) Text( - text = age, + text = age.toString(), style = PieceTheme.typography.headingSSB, color = PieceTheme.colors.black, ) @@ -336,7 +336,7 @@ private fun BasicInfoCard( text = { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = height, + text = height.toString(), style = PieceTheme.typography.headingSSB, color = PieceTheme.colors.black, ) @@ -357,7 +357,7 @@ private fun BasicInfoCard( text = { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = weight, + text = weight.toString(), style = PieceTheme.typography.headingSSB, color = PieceTheme.colors.black, ) @@ -381,8 +381,8 @@ private fun BasicInfoCard( .padding(bottom = 32.dp), ) { InfoItem( - title = stringResource(R.string.basicinfocard_activityRegion), - content = activityRegion, + title = stringResource(R.string.basicinfocard_location), + content = location, backgroundColor = PieceTheme.colors.light3, modifier = Modifier.size( width = 144.dp, @@ -391,15 +391,15 @@ private fun BasicInfoCard( ) InfoItem( - title = stringResource(R.string.basicinfocard_occupation), - content = occupation, + title = stringResource(R.string.basicinfocard_job), + content = job, backgroundColor = PieceTheme.colors.light3, modifier = Modifier.weight(1f), ) InfoItem( - title = stringResource(R.string.basicinfocard_smokeStatue), - content = smokeStatue, + title = stringResource(R.string.basicinfocard_smokingStatus), + content = smokingStatus, backgroundColor = PieceTheme.colors.light3, modifier = Modifier.weight(1f), ) @@ -452,12 +452,13 @@ private fun ProfileScreenPreview() { state = MainProfileState( nickName = "수줍은 수달", selfDescription = "음악과 요리를 좋아하는", - age = "14", + age = 14, birthYear = "00", - height = "100", - activityRegion = "서울 특별시", - occupation = "개발자", - smokeStatue = "흡연", + height = 100, + weight = 72, + location = "서울 특별시", + job = "개발자", + smokingStatus = "흡연", ), onBasicProfileClick = {}, onValueTalkClick = {}, diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/main/MainProfileViewModel.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/main/MainProfileViewModel.kt index 51287861..69b44428 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/main/MainProfileViewModel.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/main/MainProfileViewModel.kt @@ -5,6 +5,7 @@ import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.puzzle.domain.model.error.ErrorHelper +import com.puzzle.domain.usecase.profile.GetMyProfileBasicUseCase import com.puzzle.navigation.NavigationEvent import com.puzzle.navigation.NavigationHelper import com.puzzle.navigation.ProfileGraphDest @@ -23,6 +24,7 @@ import kotlinx.coroutines.launch class MainProfileViewModel @AssistedInject constructor( @Assisted initialState: MainProfileState, + private val getMyProfileBasicUseCase: GetMyProfileBasicUseCase, internal val navigationHelper: NavigationHelper, private val errorHelper: ErrorHelper, ) : MavericksViewModel(initialState) { @@ -32,11 +34,31 @@ class MainProfileViewModel @AssistedInject constructor( val sideEffects = _sideEffects.receiveAsFlow() init { + initMainProfile() + intents.receiveAsFlow() .onEach(::processIntent) .launchIn(viewModelScope) } + private fun initMainProfile() = viewModelScope.launch { + getMyProfileBasicUseCase().onSuccess { + setState { + copy( + selfDescription = it.description, + nickName = it.nickname, + age = it.age, + birthYear = it.birthDate, + height = it.height, + weight = it.weight, + location = it.location, + job = it.job, + smokingStatus = it.smokingStatus, + ) + } + }.onFailure { errorHelper.sendError(it) } + } + internal fun onIntent(intent: MainProfileIntent) = viewModelScope.launch { intents.send(intent) } @@ -76,4 +98,4 @@ class MainProfileViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() -} \ No newline at end of file +} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/main/contract/MainProfileState.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/main/contract/MainProfileState.kt index 3da8faa3..98e20228 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/main/contract/MainProfileState.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/main/contract/MainProfileState.kt @@ -3,13 +3,13 @@ package com.puzzle.profile.graph.main.contract import com.airbnb.mvrx.MavericksState data class MainProfileState( - val selfDescription: String = "음악과 요리를 좋아하는", - val nickName: String = "수줍은 수달", - val age: String = "25", - val birthYear: String = "00", - val height: String = "180", - val weight: String = "72", - val activityRegion: String = "서울특별시", - val occupation: String = "프리랜서", - val smokeStatue: String = "비흡연", -) : MavericksState \ No newline at end of file + val selfDescription: String = "", + val nickName: String = "", + val age: Int = 0, + val birthYear: String = "", + val height: Int = 0, + val weight: Int = 0, + val location: String = "", + val job: String = "", + val smokingStatus: String = "", +) : MavericksState diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/register/RegisterProfileScreen.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/register/RegisterProfileScreen.kt index 9773db2b..2f1a2747 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/register/RegisterProfileScreen.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/register/RegisterProfileScreen.kt @@ -68,8 +68,7 @@ internal fun RegisterProfileRoute( state = state, onSaveClick = { viewModel.onIntent(RegisterProfileIntent.OnSaveClick(it)) }, onBackClick = { viewModel.onIntent(RegisterProfileIntent.OnBackClick) }, - onProfileImageChanged = { viewModel.onIntent(RegisterProfileIntent.OnPhotoClick(it)) }, - onEditPhotoClick = { viewModel.onIntent(RegisterProfileIntent.OnEditPhotoClick(it)) }, + onProfileImageChanged = { viewModel.onIntent(RegisterProfileIntent.OnProfileImageChanged(it))}, onDuplicationCheckClick = { viewModel.onIntent(RegisterProfileIntent.OnDuplicationCheckClick) }, onNickNameChanged = { viewModel.onIntent(RegisterProfileIntent.OnNickNameChange(it)) }, onDescribeMySelfChanged = { @@ -82,13 +81,13 @@ internal fun RegisterProfileRoute( onBirthdateChanged = { viewModel.onIntent(RegisterProfileIntent.OnBirthdateChange(it)) }, onHeightChanged = { viewModel.onIntent(RegisterProfileIntent.OnHeightChange(it)) }, onWeightChanged = { viewModel.onIntent(RegisterProfileIntent.OnWeightChange(it)) }, - onSmokeStatusChanged = { viewModel.onIntent(RegisterProfileIntent.OnIsSmokeClick(it)) }, + onSmokingStatusChanged = { viewModel.onIntent(RegisterProfileIntent.OnIsSmokeClick(it)) }, onSnsActivityChanged = { viewModel.onIntent(RegisterProfileIntent.OnSnsActivityClick(it)) }, onAddContactClick = { viewModel.onIntent( RegisterProfileIntent.ShowBottomSheet { ContactBottomSheet( - usingSnsPlatform = state.usingSnsPlatforms, + usingContactType = state.usingSnsPlatforms, isEdit = false, onButtonClicked = { viewModel.onIntent(RegisterProfileIntent.OnAddContactClick(it)) @@ -101,13 +100,13 @@ internal fun RegisterProfileRoute( viewModel.onIntent( RegisterProfileIntent.ShowBottomSheet { ContactBottomSheet( - usingSnsPlatform = state.usingSnsPlatforms, - nowSnsPlatform = state.contacts[idx].snsPlatform, + usingContactType = state.usingSnsPlatforms, + nowContactType = state.contacts[idx].type, isEdit = true, onButtonClicked = { viewModel.onIntent( RegisterProfileIntent.OnContactSelect( - idx, state.contacts[idx].copy(snsPlatform = it) + idx, state.contacts[idx].copy(type = it) ) ) @@ -159,7 +158,6 @@ private fun RegisterProfileScreen( onBackClick: () -> Unit, onHomeClick: () -> Unit, onSaveClick: (RegisterProfileState) -> Unit, - onEditPhotoClick: (String) -> Unit, onProfileImageChanged: (String) -> Unit, onDuplicationCheckClick: () -> Unit, onNickNameChanged: (String) -> Unit, @@ -169,7 +167,7 @@ private fun RegisterProfileScreen( onWeightChanged: (String) -> Unit, onJobDropDownClicked: () -> Unit, onLocationDropDownClicked: () -> Unit, - onSmokeStatusChanged: (Boolean) -> Unit, + onSmokingStatusChanged: (Boolean) -> Unit, onSnsActivityChanged: (Boolean) -> Unit, onSnsPlatformChange: (Int) -> Unit, onAddContactClick: () -> Unit, @@ -212,7 +210,6 @@ private fun RegisterProfileScreen( RegisterProfileState.Page.BASIC_PROFILE -> BasicProfilePage( state = state, - onEditPhotoClick = onEditPhotoClick, onProfileImageChanged = onProfileImageChanged, onNickNameChanged = onNickNameChanged, onDescribeMySelfChanged = onDescribeMySelfChanged, @@ -221,7 +218,7 @@ private fun RegisterProfileScreen( onHeightChanged = onHeightChanged, onWeightChanged = onWeightChanged, onJobDropDownClicked = onJobDropDownClicked, - onSmokeStatusChanged = onSmokeStatusChanged, + onSmokingStatusChanged = onSmokingStatusChanged, onSnsActivityChanged = onSnsActivityChanged, onDuplicationCheckClick = onDuplicationCheckClick, onContactChange = onContactChange, @@ -289,11 +286,10 @@ private fun PreviewRegisterProfileScreen() { onWeightChanged = {}, onJobDropDownClicked = {}, onLocationDropDownClicked = {}, - onSmokeStatusChanged = {}, + onSmokingStatusChanged = {}, onSnsActivityChanged = {}, onSaveClick = { }, onBackClick = { }, - onEditPhotoClick = {}, onDuplicationCheckClick = { }, onSnsPlatformChange = {}, onAddContactClick = {}, diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/register/RegisterProfileViewModel.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/register/RegisterProfileViewModel.kt index ce28bdb9..ff852e4b 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/register/RegisterProfileViewModel.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/register/RegisterProfileViewModel.kt @@ -9,7 +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.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType import com.puzzle.domain.model.profile.ValuePickAnswer import com.puzzle.domain.model.profile.ValueTalkAnswer import com.puzzle.domain.repository.ProfileRepository @@ -63,8 +63,7 @@ class RegisterProfileViewModel @AssistedInject constructor( private fun processIntent(intent: RegisterProfileIntent) { when (intent) { is RegisterProfileIntent.OnNickNameChange -> updateNickName(intent.nickName) - is RegisterProfileIntent.OnPhotoClick -> updateProfileImage(intent.imageUri) - is RegisterProfileIntent.OnEditPhotoClick -> updateProfileImage(intent.imageUri) + is RegisterProfileIntent.OnProfileImageChanged -> updateProfileImage(intent.imageUri) is RegisterProfileIntent.OnSelfDescriptionChange -> updateDescription(intent.description) is RegisterProfileIntent.OnBirthdateChange -> updateBirthdate(intent.birthday) is RegisterProfileIntent.OnHeightChange -> updateHeight(intent.height) @@ -73,7 +72,7 @@ class RegisterProfileViewModel @AssistedInject constructor( is RegisterProfileIntent.OnRegionClick -> updateLocation(intent.region) is RegisterProfileIntent.OnIsSmokeClick -> updateIsSmoke(intent.isSmoke) is RegisterProfileIntent.OnSnsActivityClick -> updateIsSnsActive(intent.isSnsActivity) - is RegisterProfileIntent.OnAddContactClick -> addContact(intent.snsPlatform) + is RegisterProfileIntent.OnAddContactClick -> addContact(intent.contactType) is RegisterProfileIntent.OnDeleteContactClick -> deleteContact(intent.idx) is RegisterProfileIntent.OnContactSelect -> updateContact(intent.idx, intent.contact) is RegisterProfileIntent.ShowBottomSheet -> showBottomSheet(intent.content) @@ -147,8 +146,8 @@ class RegisterProfileViewModel @AssistedInject constructor( private fun updateProfileImage(imageUri: String) { setState { copy( - profileImageUri = imageUri, - profileImageUriInputState = InputState.DEFAULT, + imageUrl = imageUri, + imageUrlInputState = InputState.DEFAULT, ) } } @@ -184,7 +183,7 @@ class RegisterProfileViewModel @AssistedInject constructor( description = state.description, height = state.height.toInt(), weight = state.weight.toInt(), - imageUrl = state.profileImageUri.toString(), + imageUrl = state.imageUrl.toString(), job = state.job, location = state.location, nickname = state.nickname, @@ -229,7 +228,7 @@ class RegisterProfileViewModel @AssistedInject constructor( if (!state.isBasicProfileComplete) { setState { copy( - profileImageUriInputState = getInputState(state.profileImageUri), + imageUrlInputState = getInputState(state.imageUrl), nickNameGuideMessage = updatedNickNameGuideMessage, descriptionInputState = getInputState(state.description), birthdateInputState = getInputState(state.birthdate), @@ -366,10 +365,10 @@ class RegisterProfileViewModel @AssistedInject constructor( } } - private fun addContact(snsPlatform: SnsPlatform) { + private fun addContact(contactType: ContactType) { setState { val newContacts = contacts.toMutableList().apply { - add(Contact(snsPlatform = snsPlatform, content = "")) + add(Contact(type = contactType, content = "")) } copy( diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/register/bottomsheet/ContactBottomSheet.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/register/bottomsheet/ContactBottomSheet.kt index c7442c7f..a0b5fac6 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/register/bottomsheet/ContactBottomSheet.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/register/bottomsheet/ContactBottomSheet.kt @@ -18,17 +18,17 @@ import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceBottomSheetHeader import com.puzzle.designsystem.component.PieceBottomSheetListItemDefault import com.puzzle.designsystem.component.PieceSolidButton -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType @Composable internal fun ContactBottomSheet( - usingSnsPlatform: Set, + usingContactType: Set, isEdit: Boolean, - onButtonClicked: (SnsPlatform) -> Unit, - nowSnsPlatform: SnsPlatform? = null, + onButtonClicked: (ContactType) -> Unit, + nowContactType: ContactType? = null, ) { val scrollState = rememberScrollState() - var tempSnsPlatform by remember { mutableStateOf(nowSnsPlatform) } + var tempContactType by remember { mutableStateOf(nowContactType) } Column( modifier = Modifier @@ -45,32 +45,32 @@ internal fun ContactBottomSheet( .padding(top = 12.dp) .verticalScroll(scrollState), ) { - SnsPlatform.entries.forEach { sns -> - if (sns == SnsPlatform.UNKNOWN) return@forEach + ContactType.entries.forEach { sns -> + if (sns == ContactType.UNKNOWN) return@forEach val image = when (sns) { - SnsPlatform.KAKAO_TALK_ID -> R.drawable.ic_sns_kakao - SnsPlatform.OPEN_CHAT_URL -> R.drawable.ic_sns_openchatting - SnsPlatform.INSTAGRAM_ID -> R.drawable.ic_sns_instagram - SnsPlatform.PHONE_NUMBER -> R.drawable.ic_sns_call + ContactType.KAKAO_TALK_ID -> R.drawable.ic_sns_kakao + ContactType.OPEN_CHAT_URL -> R.drawable.ic_sns_openchatting + ContactType.INSTAGRAM_ID -> R.drawable.ic_sns_instagram + ContactType.PHONE_NUMBER -> R.drawable.ic_sns_call else -> R.drawable.ic_delete_circle } PieceBottomSheetListItemDefault( label = sns.displayName, image = image, - checked = if (isEdit) sns == tempSnsPlatform - else sns in usingSnsPlatform || sns == tempSnsPlatform, - enabled = sns !in usingSnsPlatform, - onChecked = { tempSnsPlatform = sns }, + checked = if (isEdit) sns == tempContactType + else sns in usingContactType || sns == tempContactType, + enabled = sns !in usingContactType, + onChecked = { tempContactType = sns }, ) } } PieceSolidButton( label = "적용하기", - onClick = { onButtonClicked(tempSnsPlatform!!) }, - enabled = tempSnsPlatform != null, + onClick = { onButtonClicked(tempContactType!!) }, + enabled = tempContactType != null, modifier = Modifier .fillMaxWidth() .padding(top = 12.dp, bottom = 10.dp), diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/register/contract/RegisterProfileIntent.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/register/contract/RegisterProfileIntent.kt index 49fe7e8b..06156cb9 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/register/contract/RegisterProfileIntent.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/register/contract/RegisterProfileIntent.kt @@ -2,12 +2,11 @@ package com.puzzle.profile.graph.register.contract import androidx.compose.runtime.Composable import com.puzzle.domain.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType sealed class RegisterProfileIntent { data class OnNickNameChange(val nickName: String) : RegisterProfileIntent() - data class OnEditPhotoClick(val imageUri: String) : RegisterProfileIntent() - data class OnPhotoClick(val imageUri: String) : RegisterProfileIntent() + data class OnProfileImageChanged(val imageUri: String) : RegisterProfileIntent() data object OnBackClick : RegisterProfileIntent() data class OnSaveClick(val registerProfileState: RegisterProfileState) : RegisterProfileIntent() data object OnDuplicationCheckClick : RegisterProfileIntent() @@ -21,7 +20,7 @@ sealed class RegisterProfileIntent { data class OnSnsActivityClick(val isSnsActivity: Boolean) : RegisterProfileIntent() data class OnContactSelect(val idx: Int, val contact: Contact) : RegisterProfileIntent() data class OnDeleteContactClick(val idx: Int) : RegisterProfileIntent() - data class OnAddContactClick(val snsPlatform: SnsPlatform) : RegisterProfileIntent() + data class OnAddContactClick(val contactType: ContactType) : RegisterProfileIntent() data class ShowBottomSheet(val content: @Composable () -> Unit) : RegisterProfileIntent() data object HideBottomSheet : RegisterProfileIntent() } diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/register/contract/RegisterProfileState.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/register/contract/RegisterProfileState.kt index fc9b46b0..4a0fe98e 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/register/contract/RegisterProfileState.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/register/contract/RegisterProfileState.kt @@ -9,8 +9,8 @@ import com.puzzle.profile.graph.register.model.ValueTalkRegisterRO data class RegisterProfileState( val currentPage: Page = Page.BASIC_PROFILE, - val profileImageUri: String? = null, - val profileImageUriInputState: InputState = InputState.DEFAULT, + val imageUrl: String? = null, + val imageUrlInputState: InputState = InputState.DEFAULT, val nickname: String = "", val isCheckingButtonEnabled: Boolean = false, val nickNameGuideMessage: NickNameGuideMessage = NickNameGuideMessage.LENGTH_GUIDE, @@ -42,10 +42,10 @@ data class RegisterProfileState( val isValueTalkComplete: Boolean = valueTalks.find { it.answer.isBlank() } == null - val usingSnsPlatforms = contacts.map { it.snsPlatform } + val usingSnsPlatforms = contacts.map { it.type } .toSet() - val isBasicProfileComplete: Boolean = !profileImageUri.isNullOrBlank() && + val isBasicProfileComplete: Boolean = !imageUrl.isNullOrBlank() && description.isNotBlank() && birthdate.isNotBlank() && location.isNotBlank() && diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/BasicProfilePage.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/BasicProfilePage.kt index 21bc350e..9b8cca03 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/BasicProfilePage.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/BasicProfilePage.kt @@ -50,7 +50,7 @@ import com.puzzle.designsystem.component.PieceTextInputDropDown import com.puzzle.designsystem.component.PieceTextInputSnsDropDown import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.domain.model.profile.Contact -import com.puzzle.domain.model.profile.SnsPlatform +import com.puzzle.domain.model.profile.ContactType import com.puzzle.profile.graph.basic.contract.InputState import com.puzzle.profile.graph.basic.contract.NickNameGuideMessage import com.puzzle.profile.graph.register.contract.RegisterProfileState @@ -58,7 +58,6 @@ import com.puzzle.profile.graph.register.contract.RegisterProfileState @Composable internal fun BasicProfilePage( state: RegisterProfileState, - onEditPhotoClick: (String) -> Unit, onProfileImageChanged: (String) -> Unit, onNickNameChanged: (String) -> Unit, onDescribeMySelfChanged: (String) -> Unit, @@ -67,7 +66,7 @@ internal fun BasicProfilePage( onHeightChanged: (String) -> Unit, onWeightChanged: (String) -> Unit, onJobDropDownClicked: () -> Unit, - onSmokeStatusChanged: (Boolean) -> Unit, + onSmokingStatusChanged: (Boolean) -> Unit, onSnsActivityChanged: (Boolean) -> Unit, onDuplicationCheckClick: () -> Unit, onContactChange: (Int, Contact) -> Unit, @@ -100,9 +99,8 @@ internal fun BasicProfilePage( ) PhotoContent( - profileImageUri = state.profileImageUri, - profileImageUriInputState = state.profileImageUriInputState, - onEditPhotoClick = onEditPhotoClick, + profileImageUri = state.imageUrl, + profileImageUriInputState = state.imageUrlInputState, onProfileImageChanged = onProfileImageChanged, modifier = Modifier.padding(top = 40.dp), ) @@ -181,7 +179,7 @@ internal fun BasicProfilePage( SmokeContent( isSmoke = state.isSmoke, isSmokeInputState = state.isSmokeInputState, - onSmokeStatusChanged = onSmokeStatusChanged, + onSmokingStatusChanged = onSmokingStatusChanged, modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), @@ -234,11 +232,11 @@ private fun ColumnScope.SnsPlatformContent( SectionTitle(title = stringResource(R.string.basic_profile_contact_header)) contacts.forEachIndexed { idx, contact -> - val image = when (contact.snsPlatform) { - SnsPlatform.KAKAO_TALK_ID -> R.drawable.ic_sns_kakao - SnsPlatform.OPEN_CHAT_URL -> R.drawable.ic_sns_openchatting - SnsPlatform.INSTAGRAM_ID -> R.drawable.ic_sns_instagram - SnsPlatform.PHONE_NUMBER -> R.drawable.ic_sns_call + val image = when (contact.type) { + ContactType.KAKAO_TALK_ID -> R.drawable.ic_sns_kakao + ContactType.OPEN_CHAT_URL -> R.drawable.ic_sns_openchatting + ContactType.INSTAGRAM_ID -> R.drawable.ic_sns_instagram + ContactType.PHONE_NUMBER -> R.drawable.ic_sns_call else -> R.drawable.ic_delete_circle // 임시 } @@ -343,7 +341,7 @@ private fun SnsActivityContent( private fun SmokeContent( isSmoke: Boolean?, isSmokeInputState: InputState, - onSmokeStatusChanged: (Boolean) -> Unit, + onSmokingStatusChanged: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { val isSaveFailed: Boolean = @@ -358,14 +356,14 @@ private fun SmokeContent( PieceChip( label = stringResource(R.string.basic_profile_issmoking_smoking), selected = isSmoke ?: false, - onChipClicked = { onSmokeStatusChanged(true) }, + onChipClicked = { onSmokingStatusChanged(true) }, modifier = Modifier.weight(1f), ) PieceChip( label = stringResource(R.string.basic_profile_issmoking_not_smoking), selected = isSmoke?.not() ?: false, - onChipClicked = { onSmokeStatusChanged(false) }, + onChipClicked = { onSmokingStatusChanged(false) }, modifier = Modifier.weight(1f), ) } @@ -785,7 +783,6 @@ private fun NickNameContent( @Composable private fun PhotoContent( - onEditPhotoClick: (String) -> Unit, profileImageUriInputState: InputState, onProfileImageChanged: (String) -> Unit, modifier: Modifier = Modifier, @@ -797,12 +794,7 @@ private fun PhotoContent( val singlePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), - onResult = { uri -> - if (uri != null) { - onProfileImageChanged(uri.toString()) - onEditPhotoClick(uri.toString()) - } - } + onResult = { uri -> if (uri != null) { onProfileImageChanged(uri.toString()) } } ) Column( @@ -875,14 +867,13 @@ private fun BasicProfilePagePreview() { state = RegisterProfileState(), onNickNameChanged = {}, onProfileImageChanged = {}, - onEditPhotoClick = {}, onDescribeMySelfChanged = {}, onBirthdateChanged = {}, onLocationDropDownClicked = {}, onHeightChanged = {}, onWeightChanged = {}, onJobDropDownClicked = {}, - onSmokeStatusChanged = {}, + onSmokingStatusChanged = {}, onSnsActivityChanged = {}, onDeleteClick = {}, onContactChange = { _, _ -> }, diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/ValuePickScreen.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/ValuePickScreen.kt index 450c39eb..fb23ae68 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/ValuePickScreen.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/ValuePickScreen.kt @@ -3,7 +3,6 @@ package com.puzzle.profile.graph.valuepick import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -26,18 +25,16 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LocalLifecycleOwner import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel -import com.puzzle.common.ui.repeatOnStarted +import com.puzzle.common.ui.clickable import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceChip import com.puzzle.designsystem.component.PieceSubTopBar import com.puzzle.designsystem.foundation.PieceTheme import com.puzzle.domain.model.profile.AnswerOption -import com.puzzle.profile.graph.register.model.ValuePickRegisterRO +import com.puzzle.domain.model.profile.MyValuePick import com.puzzle.profile.graph.valuepick.contract.ValuePickIntent -import com.puzzle.profile.graph.valuepick.contract.ValuePickSideEffect import com.puzzle.profile.graph.valuepick.contract.ValuePickState import com.puzzle.profile.graph.valuepick.contract.ValuePickState.ScreenState @@ -46,22 +43,11 @@ internal fun ValuePickRoute( viewModel: ValuePickViewModel = mavericksViewModel() ) { val state by viewModel.collectAsState() - val lifecycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(viewModel) { - lifecycleOwner.repeatOnStarted { - viewModel.sideEffects.collect { sideEffect -> - when (sideEffect) { - is ValuePickSideEffect.Navigate -> - viewModel.navigationHelper.navigate(sideEffect.navigationEvent) - } - } - } - } ValuePickScreen( state = state, - onSaveClick = {}, + onSaveClick = { viewModel.onIntent(ValuePickIntent.OnUpdateClick(it)) }, + onEditClick = { viewModel.onIntent(ValuePickIntent.OnEditClick) }, onBackClick = { viewModel.onIntent(ValuePickIntent.OnBackClick) }, ) } @@ -69,20 +55,19 @@ internal fun ValuePickRoute( @Composable private fun ValuePickScreen( state: ValuePickState, - onSaveClick: (List) -> Unit, + onSaveClick: (List) -> Unit, + onEditClick: () -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { - var screenState: ScreenState by remember { mutableStateOf(ScreenState.SAVED) } - var valuePicks: List by remember { mutableStateOf(state.valuePicks) } + var valuePicks: List by remember { mutableStateOf(state.valuePicks) } var isContentEdited: Boolean by remember { mutableStateOf(false) } - BackHandler { - if (screenState == ScreenState.EDITING) { - // TODO : 데이터 초기화 - screenState = ScreenState.SAVED - } else { - onBackClick() + BackHandler { onBackClick() } + + LaunchedEffect(state.screenState) { + if (state.screenState == ScreenState.EDITING) { + valuePicks = state.valuePicks } } @@ -92,21 +77,19 @@ private fun ValuePickScreen( .background(PieceTheme.colors.white), ) { PieceSubTopBar( - title = when (screenState) { - ScreenState.SAVED -> stringResource(R.string.value_pick_profile_topbar_title) + title = when (state.screenState) { + ScreenState.NORMAL -> stringResource(R.string.value_pick_profile_topbar_title) ScreenState.EDITING -> stringResource(R.string.value_pick_edit_profile_topbar_title) }, onNavigationClick = onBackClick, rightComponent = { - when (screenState) { - ScreenState.SAVED -> + when (state.screenState) { + ScreenState.NORMAL -> Text( text = stringResource(R.string.value_pick_profile_topbar_edit), style = PieceTheme.typography.bodyMM, color = PieceTheme.colors.primaryDefault, - modifier = Modifier.clickable { - screenState = ScreenState.EDITING - }, + modifier = Modifier.clickable { onEditClick() }, ) ScreenState.EDITING -> @@ -118,13 +101,9 @@ private fun ValuePickScreen( } else { PieceTheme.colors.dark3 }, - modifier = Modifier.clickable { - if (isContentEdited) { - onSaveClick(valuePicks) - isContentEdited = false - } - - screenState = ScreenState.SAVED + modifier = Modifier.clickable(enabled = isContentEdited) { + onSaveClick(valuePicks) + isContentEdited = false }, ) } @@ -139,11 +118,11 @@ private fun ValuePickScreen( ValuePickCards( valuePicks = valuePicks, - screenState = screenState, - onContentChange = { + screenState = state.screenState, + onContentChange = { changedValuePick -> valuePicks = valuePicks.map { valuePick -> - if (valuePick.id == it.id) { - it + if (valuePick.id == changedValuePick.id) { + changedValuePick } else { valuePick } @@ -157,9 +136,9 @@ private fun ValuePickScreen( @Composable private fun ValuePickCards( - valuePicks: List, + valuePicks: List, screenState: ScreenState, - onContentChange: (ValuePickRegisterRO) -> Unit, + onContentChange: (MyValuePick) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn(modifier = modifier.fillMaxSize()) { @@ -187,9 +166,9 @@ private fun ValuePickCards( @Composable private fun ValuePickCard( - item: ValuePickRegisterRO, + item: MyValuePick, screenState: ScreenState, - onContentChange: (ValuePickRegisterRO) -> Unit, + onContentChange: (MyValuePick) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -247,7 +226,7 @@ private fun ValuePickPreview() { ValuePickScreen( state = ValuePickState( valuePicks = listOf( - ValuePickRegisterRO( + MyValuePick( id = 0, category = "음주", question = "사귀는 사람과 함께 술을 마시는 것을 좋아하나요?", @@ -263,7 +242,7 @@ private fun ValuePickPreview() { ) ), ), - ValuePickRegisterRO( + MyValuePick( id = 1, category = "만남 빈도", question = "주말에 얼마나 자주 데이트를 하고싶나요?", @@ -279,7 +258,7 @@ private fun ValuePickPreview() { ) ), ), - ValuePickRegisterRO( + MyValuePick( id = 2, category = "연락 빈도", question = "연인 사이에 얼마나 자주 연락하는게 좋은가요?", @@ -295,7 +274,7 @@ private fun ValuePickPreview() { ) ), ), - ValuePickRegisterRO( + MyValuePick( id = 3, category = "연락 방식", question = "연락할 때 어떤 방법을 더 좋아하나요?", @@ -311,7 +290,7 @@ private fun ValuePickPreview() { ) ), ), - ValuePickRegisterRO( + MyValuePick( id = 4, category = "데이트", question = "공공장소에서 연인 티를 내는 것에 대해 어떻게 생각하나요?", @@ -327,7 +306,7 @@ private fun ValuePickPreview() { ) ), ), - ValuePickRegisterRO( + MyValuePick( id = 5, category = "장거리 연애", question = "장거리 연애에 대해 어떻게 생각하나요?", @@ -343,7 +322,7 @@ private fun ValuePickPreview() { ) ), ), - ValuePickRegisterRO( + MyValuePick( id = 6, category = "SNS", question = "연인이 활발한 SNS 활동을 하거나 게스타라면 기분이 어떨 것 같나요?", @@ -362,6 +341,7 @@ private fun ValuePickPreview() { ) ), onBackClick = {}, + onEditClick = {}, onSaveClick = {}, ) } diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/ValuePickViewModel.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/ValuePickViewModel.kt index 458f5084..3377ef1f 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/ValuePickViewModel.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/ValuePickViewModel.kt @@ -5,10 +5,12 @@ import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.puzzle.domain.model.error.ErrorHelper +import com.puzzle.domain.model.profile.MyValuePick +import com.puzzle.domain.repository.ProfileRepository +import com.puzzle.domain.usecase.profile.GetMyValuePicksUseCase import com.puzzle.navigation.NavigationEvent import com.puzzle.navigation.NavigationHelper import com.puzzle.profile.graph.valuepick.contract.ValuePickIntent -import com.puzzle.profile.graph.valuepick.contract.ValuePickSideEffect import com.puzzle.profile.graph.valuepick.contract.ValuePickState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -22,28 +24,59 @@ import kotlinx.coroutines.launch class ValuePickViewModel @AssistedInject constructor( @Assisted initialState: ValuePickState, + private val getMyValuePicksUseCase: GetMyValuePicksUseCase, + private val profileRepository: ProfileRepository, internal val navigationHelper: NavigationHelper, private val errorHelper: ErrorHelper, ) : MavericksViewModel(initialState) { - private val intents = Channel(BUFFERED) - private val _sideEffects = Channel(BUFFERED) - val sideEffects = _sideEffects.receiveAsFlow() init { + initValuePick() + intents.receiveAsFlow() .onEach(::processIntent) .launchIn(viewModelScope) } + private fun initValuePick() = viewModelScope.launch { + getMyValuePicksUseCase().onSuccess { + setState { copy(valuePicks = it) } + }.onFailure { errorHelper.sendError(it) } + } + internal fun onIntent(intent: ValuePickIntent) = viewModelScope.launch { intents.send(intent) } private suspend fun processIntent(intent: ValuePickIntent) { when (intent) { - ValuePickIntent.OnBackClick -> _sideEffects.send( - ValuePickSideEffect.Navigate(NavigationEvent.NavigateUp) + is ValuePickIntent.OnUpdateClick -> updateValuePicks(intent.newValuePicks) + ValuePickIntent.OnEditClick -> setEditMode() + ValuePickIntent.OnBackClick -> processOnBackClick() + } + } + + private fun setEditMode() = setState { copy(screenState = ValuePickState.ScreenState.EDITING) } + + private fun updateValuePicks(valuePicks: List) = viewModelScope.launch { + profileRepository.updateMyValuePicks(valuePicks) + .onSuccess { + setState { + copy( + valuePicks = it, + screenState = ValuePickState.ScreenState.NORMAL, + ) + } + } + .onFailure { errorHelper.sendError(it) } + } + + private fun processOnBackClick() = withState { state -> + when (state.screenState) { + ValuePickState.ScreenState.EDITING -> setState { copy(screenState = ValuePickState.ScreenState.NORMAL) } + ValuePickState.ScreenState.NORMAL -> navigationHelper.navigate( + NavigationEvent.NavigateUp ) } } @@ -56,4 +89,4 @@ class ValuePickViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() -} \ No newline at end of file +} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickIntent.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickIntent.kt index 5cb7dcbf..dd67c356 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickIntent.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickIntent.kt @@ -1,5 +1,9 @@ package com.puzzle.profile.graph.valuepick.contract +import com.puzzle.domain.model.profile.MyValuePick + sealed class ValuePickIntent { + data class OnUpdateClick(val newValuePicks: List) : ValuePickIntent() + data object OnEditClick : ValuePickIntent() data object OnBackClick : ValuePickIntent() -} \ No newline at end of file +} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickSideEffect.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickSideEffect.kt deleted file mode 100644 index 8e34c238..00000000 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickSideEffect.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.puzzle.profile.graph.valuepick.contract - -import com.puzzle.navigation.NavigationEvent - -sealed class ValuePickSideEffect { - data class Navigate(val navigationEvent: NavigationEvent) : ValuePickSideEffect() -} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickState.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickState.kt index d1f00ed9..16a080af 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickState.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/valuepick/contract/ValuePickState.kt @@ -1,14 +1,15 @@ package com.puzzle.profile.graph.valuepick.contract import com.airbnb.mvrx.MavericksState -import com.puzzle.profile.graph.register.model.ValuePickRegisterRO +import com.puzzle.domain.model.profile.MyValuePick data class ValuePickState( - val valuePicks: List = emptyList(), + val screenState: ScreenState = ScreenState.NORMAL, + val valuePicks: List = emptyList(), ) : MavericksState { enum class ScreenState { EDITING, - SAVED + NORMAL } } diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/ValueTalkScreen.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/ValueTalkScreen.kt index 1c20c229..a2465a29 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/ValueTalkScreen.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/ValueTalkScreen.kt @@ -37,25 +37,20 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LocalLifecycleOwner import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import com.puzzle.common.ui.clickable -import com.puzzle.common.ui.repeatOnStarted import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceSubTopBar import com.puzzle.designsystem.component.PieceTextInputAI import com.puzzle.designsystem.component.PieceTextInputLong import com.puzzle.designsystem.foundation.PieceTheme -import com.puzzle.domain.model.profile.ValueTalkQuestion -import com.puzzle.profile.graph.register.model.ValueTalkRegisterRO +import com.puzzle.domain.model.profile.MyValueTalk import com.puzzle.profile.graph.valuetalk.contract.ValueTalkIntent -import com.puzzle.profile.graph.valuetalk.contract.ValueTalkSideEffect import com.puzzle.profile.graph.valuetalk.contract.ValueTalkState import com.puzzle.profile.graph.valuetalk.contract.ValueTalkState.Companion.PAGE_TRANSITION_DURATION import com.puzzle.profile.graph.valuetalk.contract.ValueTalkState.Companion.TEXT_DISPLAY_DURATION import com.puzzle.profile.graph.valuetalk.contract.ValueTalkState.ScreenState -import com.puzzle.profile.graph.valuetalk.model.MyValueTalkRO import kotlinx.coroutines.delay @Composable @@ -63,22 +58,11 @@ internal fun ValueTalkRoute( viewModel: ValueTalkViewModel = mavericksViewModel(), ) { val state by viewModel.collectAsState() - val lifecycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(viewModel) { - lifecycleOwner.repeatOnStarted { - viewModel.sideEffects.collect { sideEffect -> - when (sideEffect) { - is ValueTalkSideEffect.Navigate -> - viewModel.navigationHelper.navigate(sideEffect.navigationEvent) - } - } - } - } ValueTalkScreen( state = state, onBackClick = { viewModel.onIntent(ValueTalkIntent.OnBackClick) }, + onEditClick = {}, onSaveClick = {}, onAiSummarySaveClick = {}, ) @@ -87,22 +71,21 @@ internal fun ValueTalkRoute( @Composable private fun ValueTalkScreen( state: ValueTalkState, - onSaveClick: (List) -> Unit, + onSaveClick: (List) -> Unit, + onEditClick: () -> Unit, onBackClick: () -> Unit, - onAiSummarySaveClick: (MyValueTalkRO) -> Unit, + onAiSummarySaveClick: (MyValueTalk) -> Unit, modifier: Modifier = Modifier, ) { - var screenState: ScreenState by remember { mutableStateOf(ScreenState.SAVED) } - var valueTalkQuestions: List by remember { mutableStateOf(state.valueTalkQuestions) } + var valueTalks: List by remember { mutableStateOf(state.valueTalks) } var isContentEdited: Boolean by remember { mutableStateOf(false) } - var editedValueTalkLabels: List by remember { mutableStateOf(emptyList()) } + var editedValueTalkIds: List by remember { mutableStateOf(emptyList()) } - BackHandler { - if (screenState == ScreenState.EDITING) { - // TODO : 데이터 초기화 - screenState = ScreenState.SAVED - } else { - onBackClick() + BackHandler { onBackClick() } + + LaunchedEffect(state.screenState) { + if (state.screenState == ScreenState.EDITING) { + valueTalks = state.valueTalks } } @@ -112,74 +95,62 @@ private fun ValueTalkScreen( .background(PieceTheme.colors.white), ) { PieceSubTopBar( - title = when (screenState) { - ScreenState.SAVED -> stringResource(R.string.value_talk_profile_topbar_title) + title = when (state.screenState) { + ScreenState.NORMAL -> stringResource(R.string.value_talk_profile_topbar_title) ScreenState.EDITING -> stringResource(R.string.value_talk_edit_profile_topbar_title) }, onNavigationClick = onBackClick, rightComponent = { - when (screenState) { - ScreenState.SAVED -> - Text( - text = stringResource(R.string.value_talk_profile_topbar_edit), - style = PieceTheme.typography.bodyMM, - color = PieceTheme.colors.primaryDefault, - modifier = Modifier.clickable { - screenState = ScreenState.EDITING - }, - ) + when (state.screenState) { + ScreenState.NORMAL -> Text( + text = stringResource(R.string.value_talk_profile_topbar_edit), + style = PieceTheme.typography.bodyMM, + color = PieceTheme.colors.primaryDefault, + modifier = Modifier.clickable { onEditClick() }, + ) - ScreenState.EDITING -> - Text( - text = stringResource(R.string.value_talk_profile_topbar_save), - style = PieceTheme.typography.bodyMM, - color = if (isContentEdited) { - PieceTheme.colors.primaryDefault - } else { - PieceTheme.colors.dark3 - }, - modifier = Modifier.clickable { - if (isContentEdited) { - valueTalkQuestions = valueTalkQuestions.map { valueTalk -> - if (editedValueTalkLabels.contains(valueTalk.category)) { - valueTalk.copy(summary = "") - } else { - valueTalk - } - } - onSaveClick(valueTalkQuestions) - isContentEdited = false - editedValueTalkLabels = emptyList() + ScreenState.EDITING -> Text( + text = stringResource(R.string.value_talk_profile_topbar_save), + style = PieceTheme.typography.bodyMM, + color = if (isContentEdited) { + PieceTheme.colors.primaryDefault + } else { + PieceTheme.colors.dark3 + }, + modifier = Modifier.clickable(enabled = isContentEdited) { + valueTalks = valueTalks.map { valueTalk -> + if (editedValueTalkIds.contains(valueTalk.id)) { + valueTalk.copy(summary = "") + } else { + valueTalk } - screenState = ScreenState.SAVED - }, - ) + } + + onSaveClick(valueTalks) + isContentEdited = false + editedValueTalkIds = emptyList() + }, + ) } }, modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 20.dp, - vertical = 14.dp, - ), + .padding(horizontal = 20.dp, vertical = 14.dp), ) ValueTalkCards( - valueTalks = valueTalkQuestions, - screenState = screenState, + valueTalks = valueTalks, + screenState = state.screenState, onContentChange = { editedValueTalk -> - valueTalkQuestions = valueTalkQuestions.map { valueTalk -> - if (valueTalk.category == editedValueTalk.category) { - if (!editedValueTalkLabels.contains(valueTalk.category)) { - editedValueTalkLabels = editedValueTalkLabels + valueTalk.category - } + valueTalks = valueTalks.map { valueTalk -> + if (valueTalk.id == editedValueTalk.id) { valueTalk.copy(answer = editedValueTalk.answer) } else { valueTalk } } - isContentEdited = valueTalkQuestions != state.valueTalkQuestions + isContentEdited = valueTalks != state.valueTalks }, onAiSummarySaveClick = onAiSummarySaveClick, ) @@ -188,10 +159,10 @@ private fun ValueTalkScreen( @Composable private fun ValueTalkCards( - valueTalks: List, + valueTalks: List, screenState: ScreenState, - onContentChange: (MyValueTalkRO) -> Unit, - onAiSummarySaveClick: (MyValueTalkRO) -> Unit, + onContentChange: (MyValueTalk) -> Unit, + onAiSummarySaveClick: (MyValueTalk) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn(modifier = modifier.fillMaxSize()) { @@ -201,10 +172,7 @@ private fun ValueTalkCards( screenState = screenState, onContentChange = onContentChange, onAiSummarySaveClick = onAiSummarySaveClick, - modifier = Modifier.padding( - horizontal = 20.dp, - vertical = 24.dp, - ) + modifier = Modifier.padding(horizontal = 20.dp, vertical = 24.dp), ) if (idx < valueTalks.size - 1) { @@ -220,10 +188,10 @@ private fun ValueTalkCards( @Composable private fun ValueTalkCard( - item: MyValueTalkRO, + item: MyValueTalk, screenState: ScreenState, - onContentChange: (MyValueTalkRO) -> Unit, - onAiSummarySaveClick: (MyValueTalkRO) -> Unit, + onContentChange: (MyValueTalk) -> Unit, + onAiSummarySaveClick: (MyValueTalk) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -243,38 +211,34 @@ private fun ValueTalkCard( PieceTextInputLong( value = item.answer, - onValueChange = { - onContentChange(item.copy(answer = it)) - }, + onValueChange = { onContentChange(item.copy(answer = it)) }, limit = 300, readOnly = when (screenState) { - ScreenState.SAVED -> true + ScreenState.NORMAL -> true ScreenState.EDITING -> false }, ) when (screenState) { - ScreenState.SAVED -> - AiSummaryContent( - item = item, - onAiSummarySaveClick = onAiSummarySaveClick, - ) + ScreenState.NORMAL -> AiSummaryContent( + item = item, + onAiSummarySaveClick = onAiSummarySaveClick, + ) - ScreenState.EDITING -> - GuideRow( - guides = item.guides, - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp) - ) + ScreenState.EDITING -> GuideRow( + guides = item.guides, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + ) } } } @Composable private fun AiSummaryContent( - item: MyValueTalkRO, - onAiSummarySaveClick: (MyValueTalkRO) -> Unit + item: MyValueTalk, + onAiSummarySaveClick: (MyValueTalk) -> Unit ) { var editableAiSummary: String by remember { mutableStateOf(item.summary) } @@ -400,8 +364,8 @@ private fun ValueTalkPreview() { PieceTheme { ValueTalkScreen( state = ValueTalkState( - valueTalkQuestions = listOf( - MyValueTalkRO( + valueTalks = listOf( + MyValueTalk( id = 1, category = "연애관", title = "어떠한 사람과 어떠한 연애를 하고 싶은지 들려주세요", @@ -415,7 +379,7 @@ private fun ValueTalkPreview() { "연인 관계를 통해 어떤 가치를 얻고 싶나요?", ), ), - MyValueTalkRO( + MyValueTalk( id = 2, category = "관심사와 취향", title = "무엇을 할 때 가장 행복한가요?\n요즘 어떠한 것에 관심을 두고 있나요?", @@ -429,7 +393,7 @@ private fun ValueTalkPreview() { "요즘 마음을 사로잡은 콘텐츠를 공유해 보세요", ), ), - MyValueTalkRO( + MyValueTalk( id = 3, category = "꿈과 목표", title = "어떤 일을 하며 무엇을 목표로 살아가나요?\n인생에서 이루고 싶은 꿈은 무엇인가요?", @@ -447,6 +411,7 @@ private fun ValueTalkPreview() { ), onBackClick = {}, onSaveClick = {}, + onEditClick = {}, onAiSummarySaveClick = {}, ) } diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/ValueTalkViewModel.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/ValueTalkViewModel.kt index ef45dd3c..0473c0ae 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/ValueTalkViewModel.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/ValueTalkViewModel.kt @@ -5,10 +5,12 @@ import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.puzzle.domain.model.error.ErrorHelper +import com.puzzle.domain.model.profile.MyValueTalk +import com.puzzle.domain.repository.ProfileRepository +import com.puzzle.domain.usecase.profile.GetMyValueTalksUseCase import com.puzzle.navigation.NavigationEvent import com.puzzle.navigation.NavigationHelper import com.puzzle.profile.graph.valuetalk.contract.ValueTalkIntent -import com.puzzle.profile.graph.valuetalk.contract.ValueTalkSideEffect import com.puzzle.profile.graph.valuetalk.contract.ValueTalkState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -22,32 +24,63 @@ import kotlinx.coroutines.launch class ValueTalkViewModel @AssistedInject constructor( @Assisted initialState: ValueTalkState, + private val getMyValueTalksUseCase: GetMyValueTalksUseCase, + private val profileRepository: ProfileRepository, internal val navigationHelper: NavigationHelper, private val errorHelper: ErrorHelper, ) : MavericksViewModel(initialState) { private val intents = Channel(BUFFERED) - private val _sideEffects = Channel(BUFFERED) - val sideEffects = _sideEffects.receiveAsFlow() init { + initValueTalk() + intents.receiveAsFlow() .onEach(::processIntent) .launchIn(viewModelScope) } + private fun initValueTalk() = viewModelScope.launch { + getMyValueTalksUseCase().onSuccess { + setState { copy(valueTalks = it) } + }.onFailure { errorHelper.sendError(it) } + } + internal fun onIntent(intent: ValueTalkIntent) = viewModelScope.launch { intents.send(intent) } private suspend fun processIntent(intent: ValueTalkIntent) { when (intent) { - ValueTalkIntent.OnBackClick -> _sideEffects.send( - ValueTalkSideEffect.Navigate(NavigationEvent.NavigateUp) - ) + ValueTalkIntent.OnBackClick -> processBackClick() + ValueTalkIntent.OnEditClick -> setEditMode() + is ValueTalkIntent.OnUpdateClick -> updateValueTalk(intent.newValueTalks) } } + private fun processBackClick() = withState { state -> + when (state.screenState) { + ValueTalkState.ScreenState.NORMAL -> navigationHelper.navigate(NavigationEvent.NavigateUp) + ValueTalkState.ScreenState.EDITING -> + setState { copy(screenState = ValueTalkState.ScreenState.NORMAL) } + } + } + + private fun setEditMode() = setState { copy(screenState = ValueTalkState.ScreenState.EDITING) } + + private fun updateValueTalk(valueTalks: List) = viewModelScope.launch { + profileRepository.updateMyValueTalks(valueTalks) + .onSuccess { + setState { + copy( + screenState = ValueTalkState.ScreenState.NORMAL, + valueTalks = valueTalks + ) + } + } + .onFailure { errorHelper.sendError(it) } + } + @AssistedFactory interface Factory : AssistedViewModelFactory { override fun create(state: ValueTalkState): ValueTalkViewModel @@ -56,4 +89,4 @@ class ValueTalkViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() -} \ No newline at end of file +} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkIntent.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkIntent.kt index 4130f71b..9d32ca3a 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkIntent.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkIntent.kt @@ -1,5 +1,9 @@ package com.puzzle.profile.graph.valuetalk.contract +import com.puzzle.domain.model.profile.MyValueTalk + sealed class ValueTalkIntent { + data class OnUpdateClick(val newValueTalks: List) : ValueTalkIntent() + data object OnEditClick : ValueTalkIntent() data object OnBackClick : ValueTalkIntent() -} \ No newline at end of file +} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkSideEffect.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkSideEffect.kt deleted file mode 100644 index 7ed5222c..00000000 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkSideEffect.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.puzzle.profile.graph.valuetalk.contract - -import com.puzzle.navigation.NavigationEvent - -sealed class ValueTalkSideEffect { - data class Navigate(val navigationEvent: NavigationEvent) : ValueTalkSideEffect() -} diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkState.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkState.kt index 107c4582..f1fc920f 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkState.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/contract/ValueTalkState.kt @@ -1,53 +1,11 @@ package com.puzzle.profile.graph.valuetalk.contract import com.airbnb.mvrx.MavericksState -import com.puzzle.profile.graph.valuetalk.model.MyValueTalkRO +import com.puzzle.domain.model.profile.MyValueTalk data class ValueTalkState( - val valueTalkQuestions: List = listOf( - MyValueTalkRO( - id = 1, - category = "연애관", - title = "어떠한 사람과 어떠한 연애를 하고 싶은지 들려주세요", - answer = "저는 연애에서 서로의 존중과 신뢰가 가장 중요하다고 생각합니다. 진정한 소통을 통해 서로의 감정을 이해하고, 함께 성장할 수 있는 관계를 원합니다. 일상 속 작은 것에도 감사하며, 서로의 꿈과 목표를 지지하고 응원하는 파트너가 되고 싶습니다. 또한, 유머와 즐거움을 잃지 않으며, 함께하는 순간들을 소중히 여기고 싶습니다. 사랑은 서로를 더 나은 사람으로 만들어주는 힘이 있다고 믿습니다. 서로에게 긍정적인 영향을 주며 행복한 시간을 함께하고 싶습니다!", - summary = "신뢰하며, 함께 성장하고 싶어요.", - guides = listOf( - "함께 하고 싶은 데이트 스타일은 무엇인가요?", - "이상적인 관계의 모습을 적어 보세요", - "연인과 함께 만들고 싶은 추억이 있나요?", - "연애에서 가장 중요시하는 가치는 무엇인가요?", - "연인 관계를 통해 어떤 가치를 얻고 싶나요?", - ), - ), - MyValueTalkRO( - id = 2, - category = "관심사와 취향", - title = "무엇을 할 때 가장 행복한가요?\n요즘 어떠한 것에 관심을 두고 있나요?", - answer = "저는 다양한 취미와 관심사를 가진 사람입니다. 음악을 사랑하여 콘서트에 자주 가고, 특히 인디 음악과 재즈에 매력을 느낍니다. 요리도 좋아해 새로운 레시피에 도전하는 것을 즐깁니다. 여행을 통해 새로운 맛과 문화를 경험하는 것도 큰 기쁨입니다. 또, 자연을 사랑해서 주말마다 하이킹이나 캠핑을 자주 떠납니다. 영화와 책도 좋아해, 좋은 이야기와 감동을 나누는 시간을 소중히 여깁니다. 서로의 취향을 공유하며 즐거운 시간을 보낼 수 있기를 기대합니다!", - summary = "음악, 요리, 하이킹을 좋아해요.", - guides = listOf( - "당신의 삶을 즐겁게 만드는 것들은 무엇인가요?", - "일상에서 소소한 행복을 느끼는 순간을 적어보세요", - "최근에 몰입했던 취미가 있다면 소개해 주세요", - "최근 마음이 따뜻해졌던 순간을 들려주세요.", - "요즘 마음을 사로잡은 콘텐츠를 공유해 보세요", - ), - ), - MyValueTalkRO( - id = 3, - category = "꿈과 목표", - title = "어떤 일을 하며 무엇을 목표로 살아가나요?\n인생에서 이루고 싶은 꿈은 무엇인가요?", - answer = "안녕하세요! 저는 삶의 매 순간을 소중히 여기며, 꿈과 목표를 이루기 위해 노력하는 사람입니다. 제 가장 큰 꿈은 여행을 통해 다양한 문화와 사람들을 경험하고, 그 과정에서 얻은 지혜를 나누는 것입니다. 또한, LGBTQ+ 커뮤니티를 위한 긍정적인 변화를 이끌어내고 싶습니다. 내가 이루고자 하는 목표는 나 자신을 발전시키고, 사랑하는 사람들과 함께 행복한 순간들을 만드는 것입니다. 서로의 꿈을 지지하며 함께 성장할 수 있는 관계를 기대합니다!", - summary = "여행하며 LGBTQ+ 변화를 원해요.", - guides = listOf( - "당신의 직업은 무엇인가요?", - "앞으로 하고 싶은 일에 대해 이야기해주세요", - "어떤 일을 할 때 가장 큰 성취감을 느끼나요?", - "당신의 버킷리스트를 알려주세요", - "당신이 꿈꾸는 삶은 어떤 모습인가요?", - ), - ) - ) + var screenState: ScreenState = ScreenState.NORMAL, + val valueTalks: List = emptyList(), ) : MavericksState { companion object { @@ -57,6 +15,6 @@ data class ValueTalkState( enum class ScreenState { EDITING, - SAVED + NORMAL } } diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/model/MyValueTalkRO.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/model/MyValueTalkRO.kt deleted file mode 100644 index 4fbcfa41..00000000 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/valuetalk/model/MyValueTalkRO.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.puzzle.profile.graph.valuetalk.model - -data class MyValueTalkRO( - val id: Int, - val category: String, - val title: String, - val answer: String, - val summary: String, - val guides: List, -) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ce48b87..82f118a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,8 +21,6 @@ androidxFragment = "1.8.4" androidxConstraintlayout = "2.1.4" # https://developer.android.com/jetpack/androidx/releases/core androidxSplashscreen = "1.0.1" -# https://developer.android.com/training/data-storage/room -androidxRoom = "2.6.1" # https://developer.android.com/develop/ui/compose/bom/bom-mapping androidxComposeBom = "2024.12.01" # https://developer.android.com/jetpack/androidx/releases/navigation @@ -132,9 +130,6 @@ androidx-compose-foundation = { group = "androidx.compose.foundation", name = "f androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" } compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } -androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" } -androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidxRoom" } - coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutine" } coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutine" } coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutine" } diff --git a/presentation/src/main/java/com/puzzle/presentation/di/ViewModelsModule.kt b/presentation/src/main/java/com/puzzle/presentation/di/ViewModelsModule.kt index 2163d419..dea20a22 100644 --- a/presentation/src/main/java/com/puzzle/presentation/di/ViewModelsModule.kt +++ b/presentation/src/main/java/com/puzzle/presentation/di/ViewModelsModule.kt @@ -10,6 +10,7 @@ import com.puzzle.matching.graph.block.BlockViewModel import com.puzzle.matching.graph.contact.ContactViewModel import com.puzzle.matching.graph.detail.MatchingDetailViewModel import com.puzzle.matching.graph.main.MatchingViewModel +import com.puzzle.matching.graph.preview.ProfilePreviewViewModel import com.puzzle.matching.graph.report.ReportViewModel import com.puzzle.profile.graph.basic.BasicProfileViewModel import com.puzzle.profile.graph.main.MainProfileViewModel @@ -101,4 +102,9 @@ interface ViewModelsModule { @IntoMap @ViewModelKey(ContactViewModel::class) fun contactViewModelFactory(factory: ContactViewModel.Factory): AssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @ViewModelKey(ProfilePreviewViewModel::class) + fun profilePreviewViewModelFactory(factory: ProfilePreviewViewModel.Factory): AssistedViewModelFactory<*, *> } diff --git a/presentation/src/main/java/com/puzzle/presentation/ui/App.kt b/presentation/src/main/java/com/puzzle/presentation/ui/App.kt index 9f958503..81cfb48e 100644 --- a/presentation/src/main/java/com/puzzle/presentation/ui/App.kt +++ b/presentation/src/main/java/com/puzzle/presentation/ui/App.kt @@ -172,6 +172,7 @@ private val HIDDEN_BOTTOM_NAV_ROUTES = setOf( MatchingGraphDest.BlockRoute::class, MatchingGraphDest.ReportRoute::class, MatchingGraphDest.ContactRoute::class, + MatchingGraphDest.ProfilePreviewRoute::class, MatchingDetailRoute::class, ProfileGraphDest.RegisterProfileRoute::class, ProfileGraphDest.ValueTalkProfileRoute::class, diff --git a/settings.gradle.kts b/settings.gradle.kts index 7dc902c3..293a1e54 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,7 +31,6 @@ include(":core:data") include(":core:network") include(":core:navigation") include(":core:common-ui") -include(":core:database") include(":core:datastore") include(":core:common")