diff --git a/core/common/src/main/java/com/puzzle/common/TimeUtil.kt b/core/common/src/main/java/com/puzzle/common/TimeUtil.kt index 92e5140c..e31151d7 100644 --- a/core/common/src/main/java/com/puzzle/common/TimeUtil.kt +++ b/core/common/src/main/java/com/puzzle/common/TimeUtil.kt @@ -1,6 +1,7 @@ package com.puzzle.common import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException /** @@ -25,3 +26,18 @@ fun String.toBirthDate(): String { "$1-$2-$3" ) } + +/** + * LocalDateTime을 "YYYY년 MM월 DD일 HH:mm" 형식의 문자열로 변환합니다. + */ +fun LocalDateTime.toBlockSyncFormattedTime(): String { + val formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH:mm") + return this.format(formatter) +} + +/** + * YYYY-MM-DD형식의 문자열을 "YYYYMMDD" 형식의 문자열로 변환합니다. + */ +fun String.toCompactDateString(): String { + return this.replace("-", "") +} diff --git a/core/data/src/main/java/com/puzzle/data/repository/AuthRepositoryImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/AuthRepositoryImpl.kt index 1420dfd2..41ee5652 100644 --- a/core/data/src/main/java/com/puzzle/data/repository/AuthRepositoryImpl.kt +++ b/core/data/src/main/java/com/puzzle/data/repository/AuthRepositoryImpl.kt @@ -18,9 +18,9 @@ class AuthRepositoryImpl @Inject constructor( ) : AuthRepository { override suspend fun loginOauth( oAuthProvider: OAuthProvider, - token: String + oauthCredential: String ): Result = suspendRunCatching { - val response = authDataSource.loginOauth(oAuthProvider, token).getOrThrow() + val response = authDataSource.loginOauth(oAuthProvider, oauthCredential).getOrThrow() coroutineScope { val accessTokenJob = launch { 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 9c8b813c..613e2cdf 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 @@ -5,6 +5,7 @@ import com.puzzle.data.image.ImageResizer 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.AiSummary import com.puzzle.domain.model.profile.Contact import com.puzzle.domain.model.profile.MyProfileBasic import com.puzzle.domain.model.profile.MyValuePick @@ -14,10 +15,14 @@ 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 +import com.puzzle.network.api.sse.SseClient import com.puzzle.network.model.UNKNOWN_INT +import com.puzzle.network.model.profile.SseAiSummaryResponse import com.puzzle.network.source.profile.ProfileDataSource import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -27,7 +32,11 @@ class ProfileRepositoryImpl @Inject constructor( private val localProfileDataSource: LocalProfileDataSource, private val localTokenDataSource: LocalTokenDataSource, private val localUserDataSource: LocalUserDataSource, + private val sseClient: SseClient, ) : ProfileRepository { + override val aiSummary: Flow = + sseClient.aiSummaryResponse.map(SseAiSummaryResponse::toDomain) + override suspend fun loadValuePickQuestions(): Result = suspendRunCatching { val valuePickQuestions = profileDataSource.loadValuePickQuestions() .getOrThrow() @@ -139,6 +148,22 @@ class ProfileRepositoryImpl @Inject constructor( updatedProfileBasic } + override suspend fun updateAiSummary(profileTalkId: Int, summary: String): Result = + suspendRunCatching { + profileDataSource.updateAiSummary( + profileTalkId = profileTalkId, + summary = summary, + ).onSuccess { + val oldValueTalks = retrieveMyValueTalks().getOrThrow() + val newValueTalks = oldValueTalks.map { valueTalk -> + if (valueTalk.id == profileTalkId) valueTalk.copy(summary = summary) + else valueTalk + } + + localProfileDataSource.setMyValueTalks(newValueTalks) + } + } + override suspend fun checkNickname(nickname: String): Result = profileDataSource.checkNickname(nickname) @@ -195,4 +220,10 @@ class ProfileRepositoryImpl @Inject constructor( userRoleJob.join() } } + + override suspend fun connectSSE(): Result = suspendRunCatching { sseClient.connect() } + override suspend fun disconnectSSE(): Result = suspendRunCatching { + profileDataSource.disconnectSSE().getOrThrow() + sseClient.disconnect() + } } diff --git a/core/data/src/main/java/com/puzzle/data/repository/UserRepositoryImpl.kt b/core/data/src/main/java/com/puzzle/data/repository/UserRepositoryImpl.kt index 179eef5b..a0c45a6d 100644 --- a/core/data/src/main/java/com/puzzle/data/repository/UserRepositoryImpl.kt +++ b/core/data/src/main/java/com/puzzle/data/repository/UserRepositoryImpl.kt @@ -5,9 +5,11 @@ import com.puzzle.datastore.datasource.user.LocalUserDataSource import com.puzzle.domain.model.user.UserRole import com.puzzle.domain.model.user.UserSetting import com.puzzle.domain.repository.UserRepository +import com.puzzle.network.model.user.GetBlockSyncTimeResponse import com.puzzle.network.model.user.GetSettingInfoResponse import com.puzzle.network.source.user.UserDataSource import kotlinx.coroutines.flow.first +import java.time.LocalDateTime import javax.inject.Inject class UserRepositoryImpl @Inject constructor( @@ -22,6 +24,9 @@ class UserRepositoryImpl @Inject constructor( override suspend fun getUserSettingInfo(): Result = userDataSource.getSettingsInfo().mapCatching(GetSettingInfoResponse::toDomain) + override suspend fun getBlockSyncTime(): Result = + userDataSource.getBlockSyncTime().mapCatching(GetBlockSyncTimeResponse::toDomain) + override suspend fun updatePushNotification(toggle: Boolean): Result = userDataSource.updatePushNotification(toggle) diff --git a/core/data/src/test/java/com/puzzle/data/fake/FakeSseClient.kt b/core/data/src/test/java/com/puzzle/data/fake/FakeSseClient.kt new file mode 100644 index 00000000..51dc6da5 --- /dev/null +++ b/core/data/src/test/java/com/puzzle/data/fake/FakeSseClient.kt @@ -0,0 +1,18 @@ +package com.puzzle.data.fake + +import com.puzzle.network.api.sse.SseClient +import com.puzzle.network.model.profile.SseAiSummaryResponse +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class FakeSseClient : SseClient { + override val aiSummaryResponse: Flow = flow { } + + override suspend fun connect(): Result { + return Result.success(Unit) + } + + override suspend fun disconnect(): Result { + return Result.success(Unit) + } +} diff --git a/core/data/src/test/java/com/puzzle/data/fake/source/auth/FakeAuthDataSource.kt b/core/data/src/test/java/com/puzzle/data/fake/source/auth/FakeAuthDataSource.kt index 7b469f05..ca70d19b 100644 --- a/core/data/src/test/java/com/puzzle/data/fake/source/auth/FakeAuthDataSource.kt +++ b/core/data/src/test/java/com/puzzle/data/fake/source/auth/FakeAuthDataSource.kt @@ -11,7 +11,7 @@ class FakeAuthDataSource : AuthDataSource { override suspend fun loginOauth( provider: OAuthProvider, - token: String + oauthCredential: String ): Result { return Result.success(loginResponse) } 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 eab3f24b..5d8701aa 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 @@ -54,7 +54,7 @@ class FakeProfileDataSource : ProfileDataSource { ) } - return Result.success(GetMyValueTalksResponse(response = responses)) + return Result.success(GetMyValueTalksResponse(responses = responses)) } override suspend fun updateMyValuePicks(valuePicks: List): Result { @@ -73,9 +73,15 @@ class FakeProfileDataSource : ProfileDataSource { ) } - return Result.success(GetMyValuePicksResponse(response = responses)) + return Result.success(GetMyValuePicksResponse(responses = responses)) } + override suspend fun updateAiSummary( + profileTalkId: Int, + summary: String + ): Result { + return Result.success(Unit) + } override suspend fun updateMyProfileBasic( description: String, @@ -94,7 +100,7 @@ class FakeProfileDataSource : ProfileDataSource { description = description, nickname = nickname, age = null, - birthDate = birthDate, + birthdate = birthDate, height = height, weight = weight, location = location, @@ -141,6 +147,10 @@ class FakeProfileDataSource : ProfileDataSource { ) ) + override suspend fun disconnectSSE(): Result { + TODO("Not yet implemented") + } + fun setValuePicks(picks: List) { valuePicks = picks } 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 87cc726c..e32ce144 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 @@ -1,5 +1,6 @@ package com.puzzle.data.repository +import com.puzzle.data.fake.FakeSseClient import com.puzzle.data.fake.source.profile.FakeLocalProfileDataSource import com.puzzle.data.fake.source.profile.FakeProfileDataSource import com.puzzle.data.fake.source.token.FakeLocalTokenDataSource @@ -9,6 +10,7 @@ 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.api.sse.SseClient import com.puzzle.network.model.profile.ValueTalkResponse import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -24,6 +26,7 @@ class ProfileRepositoryImplTest { private lateinit var localUserDataSource: FakeLocalUserDataSource private lateinit var profileRepository: ProfileRepositoryImpl private lateinit var imageResizer: SpyImageResizer + private lateinit var sseClient: SseClient @BeforeEach fun setUp() { @@ -32,12 +35,14 @@ class ProfileRepositoryImplTest { localTokenDataSource = FakeLocalTokenDataSource() localUserDataSource = FakeLocalUserDataSource() imageResizer = SpyImageResizer() + sseClient = FakeSseClient() profileRepository = ProfileRepositoryImpl( profileDataSource = profileDataSource, localProfileDataSource = localProfileDataSource, localUserDataSource = localUserDataSource, localTokenDataSource = localTokenDataSource, imageResizer = imageResizer, + sseClient = sseClient, ) } @@ -225,4 +230,28 @@ class ProfileRepositoryImplTest { val updatedProfileBasic = result.getOrNull() assertEquals(updatedProfileBasic, storedProfileBasic) } + + @Test + fun `가치관Talk 요약 업데이트 시 로컬에 저장된다`() = runTest { + // given + val originValueTalks = listOf( + MyValueTalk( + id = 1, + category = "음주", + title = "술자리에 대한 대화", + answer = "가끔 즐깁니다.", + summary = "술자리 즐기기", + guides = emptyList(), + ) + ) + profileRepository.updateMyValueTalks(originValueTalks) + + // when + profileRepository.updateAiSummary(profileTalkId = 1, summary = "변경된 요약") + + // then + val storedValueTalks = localProfileDataSource.myValueTalks.first() + assertEquals(originValueTalks.map { it.id }, storedValueTalks.map { it.id }) + assertEquals(storedValueTalks.first { it.id == 1 }.summary, "변경된 요약") + } } diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/component/TextFields.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/component/TextFields.kt index 13d784d7..5f4da291 100644 --- a/core/designsystem/src/main/java/com/puzzle/designsystem/component/TextFields.kt +++ b/core/designsystem/src/main/java/com/puzzle/designsystem/component/TextFields.kt @@ -203,6 +203,7 @@ fun PieceTextInputAI( modifier: Modifier = Modifier, throttleTime: Long = 2000L, readOnly: Boolean = true, + limit: Int = 50, onDone: () -> Unit = {}, ) { val keyboardController = LocalSoftwareKeyboardController.current @@ -211,13 +212,10 @@ fun PieceTextInputAI( var isReadOnly: Boolean by remember { mutableStateOf(readOnly) } var isLoading: Boolean by remember { mutableStateOf(value.isBlank()) } - Column( - modifier = modifier - ) { + Column(modifier = modifier) { BasicTextField( value = value, - onValueChange = onValueChange, - singleLine = true, + onValueChange = { if (it.length <= limit) onValueChange(it) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions( onDone = { @@ -233,17 +231,18 @@ fun PieceTextInputAI( cursorBrush = SolidColor(PieceTheme.colors.primaryDefault), readOnly = isReadOnly, decorationBox = { innerTextField -> - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - if (isLoading) { - Text( - text = "작성해주신 내용을 AI가 요약하고 있어요", - style = PieceTheme.typography.bodyMM, - color = PieceTheme.colors.dark3, - ) - } else { + Row(verticalAlignment = Alignment.Bottom) { + Box(modifier = Modifier.weight(1f)) { + if (isLoading) { + Text( + text = "작성해주신 내용을 AI가 요약하고 있어요", + style = PieceTheme.typography.bodyMM, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = PieceTheme.colors.dark3, + ) + } + innerTextField() } @@ -262,9 +261,7 @@ fun PieceTextInputAI( contentDescription = null, modifier = Modifier .size(24.dp) - .clickable { - if (isLoading) return@clickable - + .clickable(enabled = !isLoading) { if (isReadOnly) { isReadOnly = false } else { @@ -277,30 +274,29 @@ fun PieceTextInputAI( }, modifier = Modifier .onFocusChanged { focusState -> isFocused = focusState.isFocused } - .height(52.dp) - .fillMaxWidth() + .heightIn(min = 52.dp) .clip(RoundedCornerShape(8.dp)) .background(PieceTheme.colors.primaryLight) .padding(horizontal = 16.dp, vertical = 14.dp), ) + } - if (!isReadOnly) { - Row { - Spacer(modifier = Modifier.weight(1f)) + if (!isReadOnly) { + Row { + Spacer(modifier = Modifier.weight(1f)) - Text( - text = buildAnnotatedString { - withStyle(style = SpanStyle(color = PieceTheme.colors.primaryDefault)) { - append(value.length.toString()) - } + Text( + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = PieceTheme.colors.primaryDefault)) { + append(value.length.toString()) + } - append("/20") - }, - style = PieceTheme.typography.bodySR, - color = PieceTheme.colors.dark3, - modifier = Modifier.padding(top = 4.dp) - ) - } + append("/${limit}") + }, + style = PieceTheme.typography.bodySR, + color = PieceTheme.colors.dark3, + modifier = Modifier.padding(top = 4.dp) + ) } } } diff --git a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Toggle.kt b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Toggle.kt index beb02fa5..b0225129 100644 --- a/core/designsystem/src/main/java/com/puzzle/designsystem/component/Toggle.kt +++ b/core/designsystem/src/main/java/com/puzzle/designsystem/component/Toggle.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.puzzle.common.ui.clickable +import com.puzzle.common.ui.throttledClickable import com.puzzle.designsystem.foundation.PieceTheme @Composable @@ -51,7 +52,7 @@ fun PieceToggle( .size(width = 34.dp, height = 20.dp) .clip(RoundedCornerShape(999.dp)) .background(if (checked) PieceTheme.colors.primaryDefault else PieceTheme.colors.light1) - .clickable { onCheckedChange() } + .throttledClickable(2000L) { onCheckedChange() } ) { Box( modifier = Modifier diff --git a/core/designsystem/src/main/res/drawable/ic_profile_generate.png b/core/designsystem/src/main/res/drawable/ic_profile_generate.png new file mode 100644 index 00000000..cc447b91 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/ic_profile_generate.png differ diff --git a/core/designsystem/src/main/res/drawable/ic_profile_generate.xml b/core/designsystem/src/main/res/drawable/ic_profile_generate.xml deleted file mode 100644 index b071d147..00000000 --- a/core/designsystem/src/main/res/drawable/ic_profile_generate.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/designsystem/src/main/res/raw/anim_piece.json b/core/designsystem/src/main/res/raw/anim_piece.json new file mode 100644 index 00000000..505a56f8 --- /dev/null +++ b/core/designsystem/src/main/res/raw/anim_piece.json @@ -0,0 +1,2443 @@ +{ + "assets": [ + { + "id": "el-13-MUX0", + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Layer 1", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 70.233, + 149.087 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 70.233, + 149.087 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 61, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "gr", + "hd": false, + "nm": "Path 1 Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "Path 1", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [ + 0, + 0 + ], + [ + -0.043, + 0 + ], + [ + 0, + 0 + ], + [ + 0.006, + -0.047 + ], + [ + -0.032, + -10.293 + ], + [ + -0.21, + -1.243 + ], + [ + 0.048, + 0 + ], + [ + 0, + 0 + ], + [ + 0.001, + 0.043 + ] + ], + "o": [ + [ + 0, + -0.043 + ], + [ + 0, + 0 + ], + [ + 0.047, + 0 + ], + [ + -0.162, + 1.243 + ], + [ + 0.031, + 10.014 + ], + [ + 0.008, + 0.048 + ], + [ + 0, + 0 + ], + [ + -0.043, + -0.001 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 43.594, + 122.251 + ], + [ + 43.672, + 122.173 + ], + [ + 59.619, + 122.173 + ], + [ + 59.697, + 122.252 + ], + [ + 57.545, + 149.445 + ], + [ + 60.33, + 175.918 + ], + [ + 60.252, + 176 + ], + [ + 43.672, + 176 + ], + [ + 43.594, + 175.922 + ] + ] + } + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 1, + 1, + 1 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + }, + { + "ty": "gr", + "hd": false, + "nm": "Path 1 Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "Path 1", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [ + 0, + 0 + ], + [ + 11.561, + 0 + ], + [ + 0, + 11.561 + ], + [ + -11.561, + 0 + ], + [ + 0, + -11.561 + ] + ], + "o": [ + [ + 0, + 11.561 + ], + [ + -11.561, + 0 + ], + [ + 0, + -11.561 + ], + [ + 11.561, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 96.873, + 143.608 + ], + [ + 75.94, + 164.54 + ], + [ + 55.008, + 143.608 + ], + [ + 75.94, + 122.676 + ], + [ + 96.873, + 143.608 + ] + ] + } + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 1, + 1, + 1 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + } + ] + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "Layer 2", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 69, + 148 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 69, + 148 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 61, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "gr", + "hd": false, + "nm": "Path 1 Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "Path 1", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [ + 0, + 0 + ], + [ + 27.614, + 0 + ], + [ + 0, + 27.614 + ], + [ + -27.614, + 0 + ], + [ + 0, + -27.614 + ] + ], + "o": [ + [ + 0, + 27.614 + ], + [ + -27.614, + 0 + ], + [ + 0, + -27.614 + ], + [ + 27.614, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 119, + 148 + ], + [ + 69, + 198 + ], + [ + 19, + 148 + ], + [ + 69, + 98 + ], + [ + 119, + 148 + ] + ] + } + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.435, + 0, + 0.984 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + } + ] + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "Layer 3", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 205.5, + 148 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 205.5, + 148 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 61, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "gr", + "hd": false, + "nm": "i Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "i", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 140.561, + 176.739 + ], + [ + 130.109, + 176.739 + ], + [ + 130.109, + 137.08 + ], + [ + 140.561, + 137.08 + ] + ] + } + } + }, + { + "ty": "sh", + "hd": false, + "nm": "i", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 1.267, + 1.157 + ], + [ + 0, + 1.682 + ], + [ + -1.214, + 1.157 + ], + [ + -1.743, + 0 + ], + [ + -1.215, + -1.209 + ], + [ + 0, + -1.735 + ], + [ + 1.214, + -1.209 + ], + [ + 1.794, + 0 + ] + ], + "o": [ + [ + -1.742, + 0 + ], + [ + -1.214, + -1.209 + ], + [ + 0, + -1.787 + ], + [ + 1.267, + -1.157 + ], + [ + 1.794, + 0 + ], + [ + 1.214, + 1.157 + ], + [ + 0, + 1.682 + ], + [ + -1.215, + 1.157 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 135.335, + 130.221 + ], + [ + 130.821, + 128.486 + ], + [ + 129, + 124.15 + ], + [ + 130.821, + 119.735 + ], + [ + 135.335, + 118 + ], + [ + 139.848, + 119.813 + ], + [ + 141.669, + 124.15 + ], + [ + 139.848, + 128.486 + ], + [ + 135.335, + 130.221 + ] + ] + } + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.435, + 0, + 0.984 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 118.318, + 2.48 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 1, + "k": [ + { + "t": 0, + "s": [ + 54.318, + 2.48 + ], + "i": { + "x": 0.3, + "y": 1.35 + }, + "o": { + "x": 0.7, + "y": -0.35 + }, + "ti": [ + 0, + 0 + ], + "to": [ + 0, + 0 + ] + }, + { + "t": 60, + "s": [ + 118.318, + 2.48 + ], + "i": { + "x": 0, + "y": 0 + }, + "o": { + "x": 1, + "y": 1 + } + } + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + }, + { + "ty": "gr", + "hd": false, + "nm": "e Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "e", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 3.167, + -1.314 + ], + [ + 3.801, + 0 + ], + [ + 3.485, + 1.787 + ], + [ + 1.953, + 3.154 + ], + [ + 0, + 4.152 + ], + [ + -1.847, + 3.153 + ], + [ + -3.22, + 1.787 + ], + [ + -4.064, + 0 + ], + [ + -3.061, + -1.787 + ], + [ + -1.742, + -3.101 + ], + [ + 0, + -4.1 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0.897, + 1.787 + ], + [ + 1.531, + 0.999 + ], + [ + 2.059, + 0 + ], + [ + 1.637, + -1.051 + ], + [ + 0.951, + -1.945 + ], + [ + 0, + -2.628 + ], + [ + -1.109, + -1.997 + ], + [ + -1.953, + -1.051 + ], + [ + -2.587, + 0 + ], + [ + -3.273, + 3.679 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -2.006, + 2.261 + ], + [ + -3.114, + 1.261 + ], + [ + -4.592, + 0 + ], + [ + -3.484, + -1.787 + ], + [ + -1.953, + -3.154 + ], + [ + 0, + -4.153 + ], + [ + 1.901, + -3.207 + ], + [ + 3.273, + -1.787 + ], + [ + 3.96, + 0 + ], + [ + 3.115, + 1.735 + ], + [ + 1.742, + 3.101 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -2.418 + ], + [ + -0.845, + -1.787 + ], + [ + -1.478, + -0.999 + ], + [ + -2.164, + 0 + ], + [ + -1.636, + 0.999 + ], + [ + -0.897, + 1.892 + ], + [ + 0, + 2.787 + ], + [ + 1.161, + 1.945 + ], + [ + 2.006, + 0.999 + ], + [ + 4.751, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 184.193, + 164.36 + ], + [ + 189.735, + 170.746 + ], + [ + 181.975, + 176.108 + ], + [ + 171.602, + 178 + ], + [ + 159.487, + 175.319 + ], + [ + 151.332, + 167.908 + ], + [ + 148.402, + 156.949 + ], + [ + 151.173, + 145.989 + ], + [ + 158.854, + 138.499 + ], + [ + 169.86, + 135.819 + ], + [ + 180.392, + 138.499 + ], + [ + 187.677, + 145.753 + ], + [ + 190.29, + 156.555 + ], + [ + 190.29, + 157.028 + ], + [ + 180.075, + 157.028 + ], + [ + 180.075, + 155.924 + ], + [ + 178.729, + 149.616 + ], + [ + 175.166, + 145.438 + ], + [ + 169.86, + 143.94 + ], + [ + 164.159, + 145.516 + ], + [ + 160.279, + 149.932 + ], + [ + 158.933, + 156.712 + ], + [ + 160.596, + 163.887 + ], + [ + 165.268, + 168.381 + ], + [ + 172.157, + 169.879 + ], + [ + 184.193, + 164.36 + ] + ] + } + } + }, + { + "ty": "sh", + "hd": false, + "nm": "e", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 190.29, + 160.102 + ], + [ + 155.132, + 160.102 + ], + [ + 155.132, + 153.48 + ], + [ + 189.023, + 153.48 + ], + [ + 190.29, + 156.555 + ] + ] + } + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.435, + 0, + 0.984 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 113.085, + 52.863 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 1, + "k": [ + { + "t": 0, + "s": [ + 29.683, + 52.863 + ], + "i": { + "x": 0.3, + "y": 1.35 + }, + "o": { + "x": 0.7, + "y": -0.35 + }, + "ti": [ + 0, + 0 + ], + "to": [ + 0, + 0 + ] + }, + { + "t": 60, + "s": [ + 113.085, + 52.863 + ], + "i": { + "x": 0, + "y": 0 + }, + "o": { + "x": 1, + "y": 1 + } + } + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + }, + { + "ty": "gr", + "hd": false, + "nm": "c Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "c", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 2.931, + -1.398 + ], + [ + 3.748, + 0 + ], + [ + 3.273, + 1.787 + ], + [ + 1.9, + 3.154 + ], + [ + 0, + 4.047 + ], + [ + -1.901, + 3.154 + ], + [ + -3.273, + 1.787 + ], + [ + -4.17, + 0 + ], + [ + -2.904, + -1.419 + ], + [ + -2.059, + -2.523 + ], + [ + 0, + 0 + ], + [ + 1.715, + 1.028 + ], + [ + 2.27, + 0 + ], + [ + 1.742, + -1.051 + ], + [ + 1.003, + -1.893 + ], + [ + 0, + -2.418 + ], + [ + -1.003, + -1.892 + ], + [ + -1.741, + -1.051 + ], + [ + -2.271, + 0 + ], + [ + -1.689, + 1.051 + ], + [ + -1.084, + 1.656 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -2.018, + 2.545 + ], + [ + -2.904, + 1.367 + ], + [ + -4.117, + 0 + ], + [ + -3.273, + -1.839 + ], + [ + -1.848, + -3.207 + ], + [ + 0, + -4.1 + ], + [ + 1.9, + -3.207 + ], + [ + 3.326, + -1.839 + ], + [ + 3.642, + 0 + ], + [ + 2.903, + 1.367 + ], + [ + 0, + 0 + ], + [ + -1.099, + -1.671 + ], + [ + -1.689, + -1.051 + ], + [ + -2.27, + 0 + ], + [ + -1.742, + 1.051 + ], + [ + -1.003, + 1.892 + ], + [ + 0, + 2.365 + ], + [ + 1.003, + 1.892 + ], + [ + 1.741, + 1.051 + ], + [ + 2.323, + 0 + ], + [ + 1.681, + -1.045 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 229.355, + 163.65 + ], + [ + 235.848, + 169.958 + ], + [ + 228.326, + 175.95 + ], + [ + 218.348, + 178 + ], + [ + 207.263, + 175.319 + ], + [ + 199.503, + 167.829 + ], + [ + 196.731, + 156.949 + ], + [ + 199.582, + 146.068 + ], + [ + 207.342, + 138.578 + ], + [ + 218.586, + 135.819 + ], + [ + 228.405, + 137.947 + ], + [ + 235.848, + 143.782 + ], + [ + 229.355, + 150.168 + ], + [ + 225.079, + 146.068 + ], + [ + 219.14, + 144.491 + ], + [ + 213.122, + 146.068 + ], + [ + 209.005, + 150.484 + ], + [ + 207.5, + 156.949 + ], + [ + 209.005, + 163.335 + ], + [ + 213.122, + 167.75 + ], + [ + 219.14, + 169.327 + ], + [ + 225.158, + 167.75 + ], + [ + 229.355, + 163.65 + ] + ] + } + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.435, + 0, + 0.984 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 163.75, + 52.863 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 1, + "k": [ + { + "t": 0, + "s": [ + 32.019, + 52.863 + ], + "i": { + "x": 0.3, + "y": 1.35 + }, + "o": { + "x": 0.7, + "y": -0.35 + }, + "ti": [ + 0, + 0 + ], + "to": [ + 0, + 0 + ] + }, + { + "t": 60, + "s": [ + 163.75, + 52.863 + ], + "i": { + "x": 0, + "y": 0 + }, + "o": { + "x": 1, + "y": 1 + } + } + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + }, + { + "ty": "gr", + "hd": false, + "nm": "e Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "e", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 3.167, + -1.314 + ], + [ + 3.801, + 0 + ], + [ + 3.483, + 1.787 + ], + [ + 1.954, + 3.153 + ], + [ + 0, + 4.152 + ], + [ + -1.847, + 3.153 + ], + [ + -3.22, + 1.787 + ], + [ + -4.065, + 0 + ], + [ + -3.062, + -1.787 + ], + [ + -1.742, + -3.101 + ], + [ + 0, + -4.1 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0.897, + 1.787 + ], + [ + 1.531, + 0.999 + ], + [ + 2.059, + 0 + ], + [ + 1.636, + -1.051 + ], + [ + 0.95, + -1.945 + ], + [ + 0, + -2.628 + ], + [ + -1.109, + -1.997 + ], + [ + -1.953, + -1.051 + ], + [ + -2.587, + 0 + ], + [ + -3.273, + 3.679 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + -2.007, + 2.261 + ], + [ + -3.115, + 1.261 + ], + [ + -4.593, + 0 + ], + [ + -3.483, + -1.787 + ], + [ + -1.953, + -3.154 + ], + [ + 0, + -4.153 + ], + [ + 1.901, + -3.207 + ], + [ + 3.273, + -1.787 + ], + [ + 3.959, + 0 + ], + [ + 3.115, + 1.735 + ], + [ + 1.742, + 3.101 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + -2.418 + ], + [ + -0.844, + -1.787 + ], + [ + -1.478, + -0.999 + ], + [ + -2.165, + 0 + ], + [ + -1.637, + 0.999 + ], + [ + -0.898, + 1.892 + ], + [ + 0, + 2.787 + ], + [ + 1.162, + 1.945 + ], + [ + 2.006, + 0.999 + ], + [ + 4.751, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 275.903, + 164.36 + ], + [ + 281.446, + 170.746 + ], + [ + 273.686, + 176.108 + ], + [ + 263.313, + 178 + ], + [ + 251.198, + 175.319 + ], + [ + 243.042, + 167.908 + ], + [ + 240.112, + 156.949 + ], + [ + 242.883, + 145.989 + ], + [ + 250.564, + 138.499 + ], + [ + 261.571, + 135.819 + ], + [ + 272.102, + 138.499 + ], + [ + 279.387, + 145.753 + ], + [ + 282, + 156.555 + ], + [ + 282, + 157.028 + ], + [ + 271.785, + 157.028 + ], + [ + 271.785, + 155.924 + ], + [ + 270.439, + 149.616 + ], + [ + 266.876, + 145.438 + ], + [ + 261.571, + 143.94 + ], + [ + 255.87, + 145.516 + ], + [ + 251.99, + 149.932 + ], + [ + 250.643, + 156.712 + ], + [ + 252.306, + 163.887 + ], + [ + 256.978, + 168.381 + ], + [ + 263.867, + 169.879 + ], + [ + 275.903, + 164.36 + ] + ] + } + } + }, + { + "ty": "sh", + "hd": false, + "nm": "e", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": true, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 282, + 160.102 + ], + [ + 246.843, + 160.102 + ], + [ + 246.843, + 153.48 + ], + [ + 280.733, + 153.48 + ], + [ + 282, + 156.555 + ] + ] + } + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.435, + 0, + 0.984 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 204.795, + 52.863 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 1, + "k": [ + { + "t": 0, + "s": [ + 29.683, + 52.863 + ], + "i": { + "x": 0.3, + "y": 1.35 + }, + "o": { + "x": 0.7, + "y": -0.35 + }, + "ti": [ + 0, + 0 + ], + "to": [ + 0, + 0 + ] + }, + { + "t": 60, + "s": [ + 204.795, + 52.863 + ], + "i": { + "x": 0, + "y": 0 + }, + "o": { + "x": 1, + "y": 1 + } + } + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + } + ] + } + ] + } + ], + "ddd": 0, + "fr": 30, + "h": 300, + "ip": 0, + "layers": [ + { + "ddd": 0, + "ind": 11, + "ty": 0, + "nm": "Import from Figma", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 150, + 150 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 150, + 150 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 61, + "st": 0, + "bm": 0, + "h": 300, + "refId": "el-13-MUX0", + "w": 300 + } + ], + "meta": { + "g": "@phase-software/lottie-exporter 0.7.0" + }, + "nm": "", + "op": 60, + "v": "5.6.0", + "w": 300 +} diff --git a/core/designsystem/src/main/res/raw/anim_setting_loading.json b/core/designsystem/src/main/res/raw/anim_setting_loading.json new file mode 100644 index 00000000..43513abc --- /dev/null +++ b/core/designsystem/src/main/res/raw/anim_setting_loading.json @@ -0,0 +1,786 @@ +{ + "assets": [ + { + "id": "el-9-Rk2V", + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Layer 1", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 12.001, + 11.969 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 12.001, + 11.969 + ] + }, + "r": { + "a": 1, + "k": [ + { + "t": 0, + "s": [ + 0 + ], + "i": { + "x": 0.72, + "y": 0.59 + }, + "o": { + "x": 0.39, + "y": 0.08 + } + }, + { + "t": 6, + "s": [ + 0 + ], + "i": { + "x": 0.72, + "y": 0.59 + }, + "o": { + "x": 0.39, + "y": 0.08 + } + }, + { + "t": 39, + "s": [ + 180 + ], + "i": { + "x": 0.72, + "y": 0.59 + }, + "o": { + "x": 0.39, + "y": 0.08 + } + }, + { + "t": 51, + "s": [ + 180 + ], + "i": { + "x": 0.72, + "y": 0.59 + }, + "o": { + "x": 0.39, + "y": 0.08 + } + }, + { + "t": 84, + "s": [ + 360 + ], + "i": { + "x": 0.72, + "y": 0.59 + }, + "o": { + "x": 0.39, + "y": 0.08 + } + }, + { + "t": 90, + "s": [ + 360 + ], + "i": { + "x": 0, + "y": 0 + }, + "o": { + "x": 1, + "y": 1 + } + } + ] + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 91, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "gr", + "hd": false, + "nm": "Path 1 (3) Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "Path 1 (3)", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + -3.562, + -4.641 + ], + [ + -0.254, + -0.614 + ] + ], + "o": [ + [ + 2.239, + -5.405 + ], + [ + 0.404, + 0.527 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 4.979, + 9.09 + ], + [ + 18.03, + 7.372 + ], + [ + 19.022, + 9.09 + ] + ] + } + } + }, + { + "ty": "sh", + "hd": false, + "nm": "Path 1 (3)", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 4.979, + 9.09 + ], + [ + 8.2, + 9.09 + ] + ] + } + } + }, + { + "ty": "sh", + "hd": false, + "nm": "Path 1 (3)", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 4.979, + 9.09 + ], + [ + 4.029, + 5.823 + ] + ] + } + } + }, + { + "ty": "st", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.424, + 0.439, + 0.451 + ] + }, + "lc": 2, + "lj": 2, + "ml": 4, + "o": { + "a": 0, + "k": 100 + }, + "w": { + "a": 0, + "k": 1.2 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + }, + { + "ty": "gr", + "hd": false, + "nm": "Path 1 (3) Group", + "bm": 0, + "it": [ + { + "ty": "sh", + "hd": false, + "nm": "Path 1 (3)", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 3.562, + 4.641 + ], + [ + 0.254, + 0.614 + ] + ], + "o": [ + [ + -2.239, + 5.405 + ], + [ + -0.404, + -0.527 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 19.023, + 14.848 + ], + [ + 5.972, + 16.566 + ], + [ + 4.98, + 14.848 + ] + ] + } + } + }, + { + "ty": "sh", + "hd": false, + "nm": "Path 1 (3)", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 19.023, + 14.848 + ], + [ + 15.802, + 14.848 + ] + ] + } + } + }, + { + "ty": "sh", + "hd": false, + "nm": "Path 1 (3)", + "d": 1, + "ks": { + "a": 0, + "k": { + "c": false, + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 19.023, + 14.848 + ], + [ + 19.973, + 18.114 + ] + ] + } + } + }, + { + "ty": "st", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 0.424, + 0.439, + 0.451 + ] + }, + "lc": 2, + "lj": 2, + "ml": 4, + "o": { + "a": 0, + "k": 100 + }, + "w": { + "a": 0, + "k": 1.2 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + } + ] + } + ] + } + ], + "ddd": 0, + "fr": 30, + "h": 24, + "ip": 0, + "layers": [ + { + "ddd": 0, + "ind": 4, + "ty": 0, + "nm": "Import from Figma", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 12, + 12 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 12, + 12 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 91, + "st": 0, + "bm": 0, + "h": 24, + "refId": "el-9-Rk2V", + "w": 24 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "Screen", + "hd": false, + "sr": 1, + "ks": { + "a": { + "a": 0, + "k": [ + 12, + 12 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 12, + 12 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "ao": 0, + "ip": 0, + "op": 91, + "st": 0, + "bm": 0, + "shapes": [ + { + "ty": "gr", + "hd": false, + "nm": "Screen Group", + "bm": 0, + "it": [ + { + "ty": "rc", + "hd": false, + "nm": "Screen", + "d": 1, + "p": { + "a": 0, + "k": [ + 12, + 12 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 24, + 24 + ] + } + }, + { + "ty": "fl", + "hd": false, + "bm": 0, + "c": { + "a": 0, + "k": [ + 1, + 1, + 1 + ] + }, + "r": 1, + "o": { + "a": 0, + "k": 100 + } + }, + { + "ty": "tr", + "nm": "Transform", + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + } + ], + "np": 0 + } + ] + } + ], + "meta": { + "g": "@phase-software/lottie-exporter 0.7.0" + }, + "nm": "", + "op": 90, + "v": "5.6.0", + "w": 24 +} diff --git a/core/domain/src/main/java/com/puzzle/domain/model/profile/AiSummary.kt b/core/domain/src/main/java/com/puzzle/domain/model/profile/AiSummary.kt new file mode 100644 index 00000000..03b97e3d --- /dev/null +++ b/core/domain/src/main/java/com/puzzle/domain/model/profile/AiSummary.kt @@ -0,0 +1,6 @@ +package com.puzzle.domain.model.profile + +data class AiSummary( + val id: Int, + val summary: String, +) 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 index c6b53e16..21f54bdf 100644 --- 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 @@ -4,7 +4,7 @@ data class MyProfileBasic( val description: String, val nickname: String, val age: Int, - val birthDate: String, + val birthdate: String, val height: Int, val weight: Int, val location: String, diff --git a/core/domain/src/main/java/com/puzzle/domain/repository/AuthRepository.kt b/core/domain/src/main/java/com/puzzle/domain/repository/AuthRepository.kt index 1726508f..69e549ac 100644 --- a/core/domain/src/main/java/com/puzzle/domain/repository/AuthRepository.kt +++ b/core/domain/src/main/java/com/puzzle/domain/repository/AuthRepository.kt @@ -11,7 +11,7 @@ interface AuthRepository { suspend fun loginOauth( oAuthProvider: OAuthProvider, - token: String + oauthCredential: String ): Result suspend fun logout(): Result 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 5a6dc5dd..22b13429 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,5 +1,6 @@ package com.puzzle.domain.repository +import com.puzzle.domain.model.profile.AiSummary import com.puzzle.domain.model.profile.Contact import com.puzzle.domain.model.profile.MyProfileBasic import com.puzzle.domain.model.profile.MyValuePick @@ -8,8 +9,11 @@ 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 kotlinx.coroutines.flow.Flow interface ProfileRepository { + val aiSummary: Flow + suspend fun loadValuePickQuestions(): Result suspend fun retrieveValuePickQuestion(): Result> @@ -41,6 +45,8 @@ interface ProfileRepository { contacts: List, ): Result + suspend fun updateAiSummary(profileTalkId: Int, summary: String): Result + suspend fun checkNickname(nickname: String): Result suspend fun uploadProfile( birthdate: String, @@ -57,4 +63,7 @@ interface ProfileRepository { valuePicks: List, valueTalks: List, ): Result + + suspend fun connectSSE(): Result + suspend fun disconnectSSE(): Result } diff --git a/core/domain/src/main/java/com/puzzle/domain/repository/UserRepository.kt b/core/domain/src/main/java/com/puzzle/domain/repository/UserRepository.kt index 004f93bd..701d9a0d 100644 --- a/core/domain/src/main/java/com/puzzle/domain/repository/UserRepository.kt +++ b/core/domain/src/main/java/com/puzzle/domain/repository/UserRepository.kt @@ -2,10 +2,12 @@ package com.puzzle.domain.repository import com.puzzle.domain.model.user.UserRole import com.puzzle.domain.model.user.UserSetting +import java.time.LocalDateTime interface UserRepository { suspend fun getUserRole(): Result suspend fun getUserSettingInfo(): Result + suspend fun getBlockSyncTime(): Result suspend fun updatePushNotification(toggle: Boolean): Result suspend fun updateMatchNotification(toggle: Boolean): Result suspend fun updateBlockAcquaintances(toggle: Boolean): Result 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 index 562d3c08..115bd848 100644 --- 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 @@ -1,5 +1,6 @@ package com.puzzle.domain.spy.repository +import com.puzzle.domain.model.profile.AiSummary import com.puzzle.domain.model.profile.Contact import com.puzzle.domain.model.profile.MyProfileBasic import com.puzzle.domain.model.profile.MyValuePick @@ -9,6 +10,7 @@ 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 +import kotlinx.coroutines.flow.Flow class SpyProfileRepository : ProfileRepository { private var localMyProfileBasic: MyProfileBasic? = null @@ -52,6 +54,9 @@ class SpyProfileRepository : ProfileRepository { remoteMyValuePicks = valuePicks } + override val aiSummary: Flow + get() = TODO("Not yet implemented") + override suspend fun loadValuePickQuestions(): Result { return Result.success(Unit) } @@ -125,6 +130,10 @@ class SpyProfileRepository : ProfileRepository { TODO("Not yet implemented") } + override suspend fun updateAiSummary(profileTalkId: Int, summary: String): Result { + TODO("Not yet implemented") + } + override suspend fun checkNickname(nickname: String): Result { TODO("Not yet implemented") } @@ -146,4 +155,12 @@ class SpyProfileRepository : ProfileRepository { ): Result { TODO("Not yet implemented") } + + override suspend fun connectSSE(): Result { + TODO("Not yet implemented") + } + + override suspend fun disconnectSSE(): Result { + TODO("Not yet implemented") + } } 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 index f25346fb..92fa6654 100644 --- 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 @@ -51,7 +51,7 @@ class GetMyProfileBasicUseCaseTest { description = "새로운 인연을 만나고 싶은 26살 개발자입니다. 여행과 커피, 그리고 좋은 대화를 좋아해요.", nickname = "코딩하는개발자", age = 26, - birthDate = "1997-09-12", + birthdate = "1997-09-12", height = 178, weight = 72, location = "서울 강남구", diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index d3ec802e..3bde2b9b 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(libs.retrofit.core) implementation(libs.retrofit.kotlin.serialization) - implementation(libs.okhttp.logging) implementation(libs.kotlinx.serialization.json) + implementation(libs.okhttp.eventsource) + implementation(libs.okhttp.logging) } 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 0cd4a0f5..80303e53 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 @@ -18,6 +18,7 @@ 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.UpdateAiSummaryRequest import com.puzzle.network.model.profile.UpdateMyProfileBasicRequest import com.puzzle.network.model.profile.UpdateMyValuePickRequests import com.puzzle.network.model.profile.UpdateMyValueTalkRequests @@ -27,10 +28,12 @@ import com.puzzle.network.model.terms.AgreeTermsRequest import com.puzzle.network.model.terms.LoadTermsResponse import com.puzzle.network.model.token.RefreshTokenRequest import com.puzzle.network.model.token.RefreshTokenResponse +import com.puzzle.network.model.user.GetBlockSyncTimeResponse import com.puzzle.network.model.user.GetSettingInfoResponse import com.puzzle.network.model.user.UpdateSettingRequest import okhttp3.MultipartBody import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.PATCH @@ -105,6 +108,12 @@ interface PieceApi { @PUT("/api/profiles/basic") suspend fun updateMyProfileBasic(@Body updateMyProfileBasicRequest: UpdateMyProfileBasicRequest): Result> + @PATCH("/api/profiles/valueTalks/{profileTalkId}/summary") + suspend fun updateAiSummary( + @Path("profileTalkId") id: Int, + @Body updateAiSummaryRequest: UpdateAiSummaryRequest, + ): Result> + @GET("/api/matches/infos") suspend fun getMatchInfo(): Result> @@ -137,4 +146,10 @@ interface PieceApi { @PUT("/api/settings/block/acquaintance") suspend fun updateBlockAcquaintances(@Body updateSettingRequest: UpdateSettingRequest): Result> + + @GET("/api/settings/blocks/contacts/sync-time") + suspend fun getBlockSyncTime(): Result> + + @DELETE("/api/sse/personal/disconnect") + suspend fun disconnectSSE(): Result> } diff --git a/core/network/src/main/java/com/puzzle/network/api/sse/SseClient.kt b/core/network/src/main/java/com/puzzle/network/api/sse/SseClient.kt new file mode 100644 index 00000000..68ef8878 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/api/sse/SseClient.kt @@ -0,0 +1,10 @@ +package com.puzzle.network.api.sse + +import com.puzzle.network.model.profile.SseAiSummaryResponse +import kotlinx.coroutines.flow.Flow + +interface SseClient { + val aiSummaryResponse: Flow + suspend fun connect(): Result + suspend fun disconnect(): Result +} diff --git a/core/network/src/main/java/com/puzzle/network/api/sse/SseClientImpl.kt b/core/network/src/main/java/com/puzzle/network/api/sse/SseClientImpl.kt new file mode 100644 index 00000000..e58f7077 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/api/sse/SseClientImpl.kt @@ -0,0 +1,43 @@ +package com.puzzle.network.api.sse + +import com.launchdarkly.eventsource.ConnectStrategy +import com.launchdarkly.eventsource.EventSource +import com.launchdarkly.eventsource.background.BackgroundEventSource +import com.puzzle.common.suspendRunCatching +import com.puzzle.network.BuildConfig +import com.puzzle.network.interceptor.TokenManager +import java.net.URL +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class SseClientImpl @Inject constructor( + private val sseEventHandler: SseEventHandler, + private val tokenManager: TokenManager, +) : SseClient { + private var sseEventSource: BackgroundEventSource? = null + override val aiSummaryResponse = sseEventHandler.sseChannel + + override suspend fun connect(): Result = suspendRunCatching { + sseEventSource = BackgroundEventSource.Builder( + sseEventHandler, + EventSource.Builder( + ConnectStrategy + .http(URL(BuildConfig.PIECE_BASE_URL + "/api/sse/personal/connect")) + .header( + "Authorization", + "Bearer ${tokenManager.getAccessToken()}" + ) + .connectTimeout(3, TimeUnit.SECONDS) + .readTimeout(600, TimeUnit.SECONDS) + ) + ).build() + + sseEventSource?.start() + } + + override suspend fun disconnect(): Result = suspendRunCatching { + sseEventSource?.close() + sseEventSource = null + } +} + diff --git a/core/network/src/main/java/com/puzzle/network/api/sse/SseEventHandler.kt b/core/network/src/main/java/com/puzzle/network/api/sse/SseEventHandler.kt new file mode 100644 index 00000000..d720f079 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/api/sse/SseEventHandler.kt @@ -0,0 +1,69 @@ +package com.puzzle.network.api.sse + +import android.util.Log +import com.launchdarkly.eventsource.MessageEvent +import com.launchdarkly.eventsource.background.BackgroundEventHandler +import com.puzzle.domain.model.error.ErrorHelper +import com.puzzle.network.model.profile.SseAiSummaryResponse +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import javax.inject.Inject + +class SseEventHandler @Inject constructor( + private val errorHelper: ErrorHelper, + private val json: Json, +) : BackgroundEventHandler { + private val _sseChannel = Channel(BUFFERED) + val sseChannel = _sseChannel.receiveAsFlow() + + override fun onOpen() { + Log.d("SseEventHandler", "SSE를 연결하였습니다.") + } + + override fun onClosed() { + Log.d("SseEventHandler", "SSE를 연결을 종료하였습니다.") + } + + override fun onMessage(event: String, messageEvent: MessageEvent) { + val eventId = messageEvent.lastEventId + val eventType = messageEvent.eventName + val eventData = messageEvent.data + + Log.d("SseEventHandler", "Event ID: $eventId") + Log.d("SseEventHandler", "Event Type: $eventType") + Log.d("SseEventHandler", "Event Data: $eventData") + + when (eventType) { + SseEventType.PROFILE_TALK_SUMMARY_MESSAGE.name -> { + try { + val response = json.decodeFromString(eventData) + _sseChannel.trySend(response) + } catch (e: SerializationException) { + runBlocking { errorHelper.sendError(e) } + } + } + + else -> Unit + } + } + + override fun onComment(comment: String) { + throw UnsupportedOperationException("사용하지 않는 함수입니다.") + } + + override fun onError(t: Throwable) { + // SSE 연결 전 또는 후 오류 발생시 처리 로직 작성 + + // 서버가 2XX 이외의 오류 응답시 com.launchdarkly.eventsource.StreamHttpErrorException: Server returned HTTP error 401 예외가 발생 + // 클라이언트에서 서버의 연결 유지 시간보다 짧게 설정시 error=com.launchdarkly.eventsource.StreamIOException: java.net.SocketTimeoutException: timeout 예외가 발생 + // 서버가 연결 유지 시간 초과로 종료시 error=com.launchdarkly.eventsource.StreamClosedByServerException: Stream closed by server 예외가 발생 + } + + enum class SseEventType { + PROFILE_TALK_SUMMARY_MESSAGE; + } +} diff --git a/core/network/src/main/java/com/puzzle/network/di/RetrofitModule.kt b/core/network/src/main/java/com/puzzle/network/di/RetrofitModule.kt index 7a4fc6d4..daa434ec 100644 --- a/core/network/src/main/java/com/puzzle/network/di/RetrofitModule.kt +++ b/core/network/src/main/java/com/puzzle/network/di/RetrofitModule.kt @@ -1,10 +1,15 @@ package com.puzzle.network.di +import com.puzzle.domain.model.error.ErrorHelper import com.puzzle.network.BuildConfig import com.puzzle.network.adapter.PieceCallAdapterFactory import com.puzzle.network.api.PieceApi +import com.puzzle.network.api.sse.SseClient +import com.puzzle.network.api.sse.SseClientImpl +import com.puzzle.network.api.sse.SseEventHandler import com.puzzle.network.authenticator.PieceAuthenticator import com.puzzle.network.interceptor.PieceInterceptor +import com.puzzle.network.interceptor.TokenManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -59,4 +64,18 @@ object RetrofitModule { .baseUrl(BuildConfig.PIECE_BASE_URL) .build() .create(PieceApi::class.java) + + @Provides + @Singleton + fun providesSseEventHandler( + json: Json, + errorHelper: ErrorHelper, + ): SseEventHandler = SseEventHandler(errorHelper, json) + + @Provides + @Singleton + fun providesSseClient( + sseEventHandler: SseEventHandler, + tokenManager: TokenManager, + ): SseClient = SseClientImpl(sseEventHandler, tokenManager) } diff --git a/core/network/src/main/java/com/puzzle/network/model/auth/LoginOauthRequest.kt b/core/network/src/main/java/com/puzzle/network/model/auth/LoginOauthRequest.kt index 8819ad0c..5d5a9cae 100644 --- a/core/network/src/main/java/com/puzzle/network/model/auth/LoginOauthRequest.kt +++ b/core/network/src/main/java/com/puzzle/network/model/auth/LoginOauthRequest.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.Serializable @Serializable data class LoginOauthRequest( val providerName: String, - val token: String, + val oauthCredential: String, ) 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 index 52e85569..67985e13 100644 --- 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 @@ -12,7 +12,7 @@ data class GetMyProfileBasicResponse( val description: String?, val nickname: String?, val age: Int?, - val birthDate: String?, + val birthdate: String?, val height: Int?, val weight: Int?, val location: String?, @@ -26,7 +26,7 @@ data class GetMyProfileBasicResponse( description = description ?: UNKNOWN_STRING, nickname = nickname ?: UNKNOWN_STRING, age = age ?: UNKNOWN_INT, - birthDate = birthDate ?: UNKNOWN_STRING, + birthdate = birthdate ?: UNKNOWN_STRING, height = height ?: UNKNOWN_INT, weight = weight ?: UNKNOWN_INT, location = location ?: 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 index 8d123d28..5a81a0c8 100644 --- 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 @@ -8,9 +8,9 @@ import kotlinx.serialization.Serializable @Serializable data class GetMyValuePicksResponse( - val response: List?, + val responses: List?, ) { - fun toDomain() = response?.map(MyValuePickResponse::toDomain) ?: emptyList() + fun toDomain() = responses?.map(MyValuePickResponse::toDomain) ?: emptyList() } @Serializable @@ -18,7 +18,7 @@ data class MyValuePickResponse( @SerialName("profileValuePickId") val id: Int?, val category: String?, val question: String?, - @SerialName("answer") val answerOptions: List?, + @SerialName("answers") val answerOptions: List?, val selectedAnswer: Int?, ) { fun toDomain() = MyValuePick( 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 index 2f85521a..625413d2 100644 --- 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 @@ -8,9 +8,9 @@ import kotlinx.serialization.Serializable @Serializable data class GetMyValueTalksResponse( - val response: List?, + val responses: List?, ) { - fun toDomain() = response?.map(MyValueTalkResponse::toDomain) ?: emptyList() + fun toDomain() = responses?.map(MyValueTalkResponse::toDomain) ?: emptyList() } @Serializable diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/GetProfileAiSummationResponse.kt b/core/network/src/main/java/com/puzzle/network/model/profile/GetProfileAiSummationResponse.kt new file mode 100644 index 00000000..24cd280d --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/GetProfileAiSummationResponse.kt @@ -0,0 +1,9 @@ +package com.puzzle.network.model.profile + +import kotlinx.serialization.Serializable + +@Serializable +data class GetProfileAiSummationResponse( + val profileValueTalkId: Int?, + val summary: String?, +) diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/SseAiSummaryResponse.kt b/core/network/src/main/java/com/puzzle/network/model/profile/SseAiSummaryResponse.kt new file mode 100644 index 00000000..fe0a2caf --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/SseAiSummaryResponse.kt @@ -0,0 +1,17 @@ +package com.puzzle.network.model.profile + +import com.puzzle.domain.model.profile.AiSummary +import com.puzzle.network.model.UNKNOWN_INT +import com.puzzle.network.model.UNKNOWN_STRING +import kotlinx.serialization.Serializable + +@Serializable +data class SseAiSummaryResponse( + val profileValueTalkId: Int?, + val summary: String?, +) { + fun toDomain() = AiSummary( + id = profileValueTalkId ?: UNKNOWN_INT, + summary = summary ?: UNKNOWN_STRING, + ) +} diff --git a/core/network/src/main/java/com/puzzle/network/model/profile/UpdateAiSummaryRequest.kt b/core/network/src/main/java/com/puzzle/network/model/profile/UpdateAiSummaryRequest.kt new file mode 100644 index 00000000..72f907d3 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/profile/UpdateAiSummaryRequest.kt @@ -0,0 +1,8 @@ +package com.puzzle.network.model.profile + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateAiSummaryRequest( + val summary: String, +) 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 index dcd88c31..77b7d0dd 100644 --- 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 @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable @Serializable data class UpdateMyValuePickRequests( - val profileValueTalkUpdateRequests: List, + val profileValuePickUpdateRequests: List, ) @Serializable diff --git a/core/network/src/main/java/com/puzzle/network/model/user/GetBlockSyncTimeResponse.kt b/core/network/src/main/java/com/puzzle/network/model/user/GetBlockSyncTimeResponse.kt new file mode 100644 index 00000000..d39e94c2 --- /dev/null +++ b/core/network/src/main/java/com/puzzle/network/model/user/GetBlockSyncTimeResponse.kt @@ -0,0 +1,14 @@ +package com.puzzle.network.model.user + +import kotlinx.serialization.Serializable +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Serializable +data class GetBlockSyncTimeResponse( + val syncTime: String?, +) { + fun toDomain(): LocalDateTime = syncTime?.let { + LocalDateTime.parse(it, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + } ?: LocalDateTime.MIN +} diff --git a/core/network/src/main/java/com/puzzle/network/source/auth/AuthDataSource.kt b/core/network/src/main/java/com/puzzle/network/source/auth/AuthDataSource.kt index a2ec5b40..4cdc1d50 100644 --- a/core/network/src/main/java/com/puzzle/network/source/auth/AuthDataSource.kt +++ b/core/network/src/main/java/com/puzzle/network/source/auth/AuthDataSource.kt @@ -5,7 +5,7 @@ import com.puzzle.network.model.auth.LoginOauthResponse import com.puzzle.network.model.auth.VerifyAuthCodeResponse interface AuthDataSource { - suspend fun loginOauth(provider: OAuthProvider, token: String): Result + suspend fun loginOauth(provider: OAuthProvider, oauthCredential: String): Result suspend fun requestAuthCode(phoneNumber: String): Result suspend fun verifyAuthCode(phoneNumber: String, code: String): Result suspend fun checkTokenHealth(token: String): Result diff --git a/core/network/src/main/java/com/puzzle/network/source/auth/AuthDataSourceImpl.kt b/core/network/src/main/java/com/puzzle/network/source/auth/AuthDataSourceImpl.kt index d803de25..9add4457 100644 --- a/core/network/src/main/java/com/puzzle/network/source/auth/AuthDataSourceImpl.kt +++ b/core/network/src/main/java/com/puzzle/network/source/auth/AuthDataSourceImpl.kt @@ -17,12 +17,12 @@ class AuthDataSourceImpl @Inject constructor( ) : AuthDataSource { override suspend fun loginOauth( provider: OAuthProvider, - token: String + oauthCredential: String ): Result = pieceApi.loginOauth( LoginOauthRequest( providerName = provider.apiValue, - token = token, + oauthCredential = oauthCredential, ) ).unwrapData() 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 fbc3a280..b04b1493 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 @@ -23,6 +23,11 @@ interface ProfileDataSource { suspend fun updateMyValueTalks(valueTalks: List): Result suspend fun updateMyValuePicks(valuePicks: List): Result + suspend fun updateAiSummary( + profileTalkId: Int, + summary: String + ): Result + suspend fun updateMyProfileBasic( description: String, nickname: String, @@ -55,4 +60,6 @@ interface ProfileDataSource { valuePicks: List, valueTalks: List, ): Result + + suspend fun disconnectSSE(): Result } 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 6bf5db49..95ae987b 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 @@ -12,6 +12,7 @@ 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.UpdateAiSummaryRequest import com.puzzle.network.model.profile.UpdateMyProfileBasicRequest import com.puzzle.network.model.profile.UpdateMyValuePickRequest import com.puzzle.network.model.profile.UpdateMyValuePickRequests @@ -74,6 +75,15 @@ class ProfileDataSourceImpl @Inject constructor( ) ).unwrapData() + override suspend fun updateAiSummary( + profileTalkId: Int, + summary: String + ): Result = + pieceApi.updateAiSummary( + id = profileTalkId, + updateAiSummaryRequest = UpdateAiSummaryRequest(summary = summary), + ).unwrapData() + override suspend fun updateMyProfileBasic( description: String, nickname: String, @@ -169,6 +179,8 @@ class ProfileDataSourceImpl @Inject constructor( ) ).unwrapData() + override suspend fun disconnectSSE(): Result = pieceApi.disconnectSSE().unwrapData() + companion object { private const val WEBP_MEDIA_TYPE = "image/webp" private const val JPEG_MEDIA_TYPE = "image/jpeg" diff --git a/core/network/src/main/java/com/puzzle/network/source/user/UserDataSource.kt b/core/network/src/main/java/com/puzzle/network/source/user/UserDataSource.kt index 1d0da7c0..3eb6eb72 100644 --- a/core/network/src/main/java/com/puzzle/network/source/user/UserDataSource.kt +++ b/core/network/src/main/java/com/puzzle/network/source/user/UserDataSource.kt @@ -2,6 +2,7 @@ package com.puzzle.network.source.user import com.puzzle.network.api.PieceApi import com.puzzle.network.model.unwrapData +import com.puzzle.network.model.user.GetBlockSyncTimeResponse import com.puzzle.network.model.user.GetSettingInfoResponse import com.puzzle.network.model.user.UpdateSettingRequest import javax.inject.Inject @@ -20,4 +21,7 @@ class UserDataSource @Inject constructor( suspend fun updateBlockAcquaintances(toggle: Boolean): Result = pieceApi.updateBlockAcquaintances(UpdateSettingRequest(toggle)).unwrapData() + + suspend fun getBlockSyncTime(): Result = + pieceApi.getBlockSyncTime().unwrapData() } diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 0024ecfd..66c1debf 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -25,6 +25,7 @@ android { dependencies { implementation(projects.core.common) + implementation(libs.lottie.compose) implementation(libs.androidx.credentials.play.services.auth) implementation(libs.google.auth) implementation(libs.kakao.user) diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginViewModel.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginViewModel.kt index ec40b828..6ec4a949 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginViewModel.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/login/LoginViewModel.kt @@ -9,13 +9,20 @@ import com.puzzle.auth.graph.login.contract.LoginSideEffect import com.puzzle.auth.graph.login.contract.LoginState import com.puzzle.domain.model.auth.OAuthProvider import com.puzzle.domain.model.error.ErrorHelper +import com.puzzle.domain.model.user.UserRole.NONE +import com.puzzle.domain.model.user.UserRole.PENDING +import com.puzzle.domain.model.user.UserRole.REGISTER +import com.puzzle.domain.model.user.UserRole.USER import com.puzzle.domain.repository.AuthRepository +import com.puzzle.domain.repository.UserRepository import com.puzzle.navigation.AuthGraphDest +import com.puzzle.navigation.MatchingGraphDest 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.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.BUFFERED import kotlinx.coroutines.flow.launchIn @@ -26,6 +33,7 @@ import kotlinx.coroutines.launch class LoginViewModel @AssistedInject constructor( @Assisted initialState: LoginState, private val authRepository: AuthRepository, + private val userRepository: UserRepository, private val navigationHelper: NavigationHelper, private val errorHelper: ErrorHelper, ) : MavericksViewModel(initialState) { @@ -60,14 +68,38 @@ class LoginViewModel @AssistedInject constructor( internal fun loginOAuth(oAuthProvider: OAuthProvider, token: String) = viewModelScope.launch { authRepository.loginOauth( oAuthProvider = oAuthProvider, - token = token, + oauthCredential = token, ).onSuccess { - navigationHelper.navigate( - NavigationEvent.NavigateTo( - route = AuthGraphDest.VerificationRoute, - popUpTo = true, + val userRole = async { userRepository.getUserRole() } + .await() + .getOrElse { return@launch } + + when (userRole) { + REGISTER -> { + navigationHelper.navigate( + NavigationEvent.NavigateTo( + route = AuthGraphDest.SignUpRoute, + popUpTo = true, + ) + ) + } + + PENDING, USER -> { + navigationHelper.navigate( + NavigationEvent.NavigateTo( + route = MatchingGraphDest.MatchingRoute, + popUpTo = true, + ) + ) + } + + NONE -> navigationHelper.navigate( + NavigationEvent.NavigateTo( + route = AuthGraphDest.VerificationRoute, + popUpTo = true, + ) ) - ) + } }.onFailure { errorHelper.sendError(it) } .also { setState { copy(isLoading = false) } } } diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/signup/page/SignUpCompletedPage.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/signup/page/SignUpCompletedPage.kt index 5c06b61d..f5adabd9 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/signup/page/SignUpCompletedPage.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/signup/page/SignUpCompletedPage.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -18,6 +19,11 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceSolidButton import com.puzzle.designsystem.foundation.PieceTheme @@ -45,7 +51,14 @@ internal fun ColumnScope.SignUpCompletedPage( .weight(1.2f), ) - Spacer( + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.anim_piece)) + val progress by animateLottieCompositionAsState( + composition = composition, + iterations = LottieConstants.IterateForever, + ) + LottieAnimation( + composition = composition, + progress = { progress }, modifier = Modifier .align(Alignment.CenterHorizontally) .size(240.dp) diff --git a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt index 32e27e3f..91c6f702 100644 --- a/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt +++ b/feature/auth/src/main/java/com/puzzle/auth/graph/verification/VerificationScreen.kt @@ -134,9 +134,7 @@ private fun VerificationScreen( PieceSubCloseTopBar( title = "", onCloseClick = { navigate(NavigationEvent.NavigateUp) }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 14.dp), + modifier = Modifier.fillMaxWidth(), ) VerificationHeader(modifier = Modifier.padding(top = 20.dp)) 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 7ab67bd1..fcedc11a 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 @@ -95,9 +95,7 @@ private fun ContactScreen( PieceSubCloseTopBar( title = "", onCloseClick = onCloseClick, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 14.dp), + modifier = Modifier.fillMaxWidth(), ) ContactBody(nickName = state.nickName) diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts index aa2b4864..1613c58c 100644 --- a/feature/profile/build.gradle.kts +++ b/feature/profile/build.gradle.kts @@ -7,6 +7,8 @@ android { } dependencies { + implementation(projects.core.common) + implementation(libs.coil.compose) implementation(libs.coil.network) implementation(libs.lottie.compose) 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 a255f781..493f1d98 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,6 +7,7 @@ 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.common.toCompactDateString import com.puzzle.domain.model.error.ErrorHelper import com.puzzle.domain.model.profile.Contact import com.puzzle.domain.model.profile.ContactType @@ -58,7 +59,7 @@ class BasicProfileViewModel @AssistedInject constructor( copy( description = it.description, nickname = it.nickname, - birthdate = it.birthDate, + birthdate = it.birthdate.toCompactDateString(), height = it.height.toString(), weight = it.weight.toString(), location = it.location, 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 69b44428..076891b1 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 @@ -48,7 +48,7 @@ class MainProfileViewModel @AssistedInject constructor( selfDescription = it.description, nickName = it.nickname, age = it.age, - birthYear = it.birthDate, + birthYear = it.birthdate.substring(2, 4), height = it.height, weight = it.weight, location = it.location, 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 1379081e..5edc6d01 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 @@ -73,11 +73,7 @@ internal fun RegisterProfileRoute( onDuplicationCheckClick = { viewModel.onIntent(RegisterProfileIntent.OnDuplicationCheckClick) }, onNickNameChanged = { viewModel.onIntent(RegisterProfileIntent.OnNickNameChange(it)) }, onDescribeMySelfChanged = { - viewModel.onIntent( - RegisterProfileIntent.OnSelfDescriptionChange( - it - ) - ) + viewModel.onIntent(RegisterProfileIntent.OnSelfDescriptionChange(it)) }, onBirthdateChanged = { viewModel.onIntent(RegisterProfileIntent.OnBirthdateChange(it)) }, onHeightChanged = { viewModel.onIntent(RegisterProfileIntent.OnHeightChange(it)) }, @@ -192,7 +188,7 @@ private fun RegisterProfileScreen( onBackClick = onBackClick, isShowBackButton = (state.currentPage != RegisterProfileState.Page.FINISH && state.currentPage != RegisterProfileState.Page.SUMMATION), - modifier = Modifier.padding(horizontal = 20.dp, vertical = 14.dp), + modifier = Modifier.padding(horizontal = 20.dp), ) if (state.currentPage != RegisterProfileState.Page.FINISH && @@ -261,12 +257,15 @@ private fun RegisterProfileScreen( else -> stringResource(R.string.next) }, onClick = { - onSaveClick( - state.copy( - valueTalks = valueTalks, - valuePicks = valuePicks, + when (state.currentPage) { + RegisterProfileState.Page.FINISH -> onCheckMyProfileClick() + else -> onSaveClick( + state.copy( + valueTalks = valueTalks, + valuePicks = valuePicks, + ) ) - ) + } }, modifier = Modifier .fillMaxWidth() 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 cb786862..b7be73c3 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 @@ -31,7 +31,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.BUFFERED -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow @@ -179,6 +178,10 @@ class RegisterProfileViewModel @AssistedInject constructor( return } + state.currentPage.getNextPage()?.let { nextPage -> + setState { copy(currentPage = nextPage) } + } + viewModelScope.launch { uploadProfileUseCase( birthdate = state.birthdate, @@ -205,9 +208,7 @@ class RegisterProfileViewModel @AssistedInject constructor( ) }, ).onSuccess { - state.currentPage.getNextPage()?.let { nextPage -> - setState { copy(currentPage = nextPage) } - } + setState { copy(currentPage = RegisterProfileState.Page.FINISH) } }.onFailure { errorHelper.sendError(it) } } } 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 ba0374ea..7f6bbf45 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 @@ -3,6 +3,7 @@ package com.puzzle.profile.graph.register.contract import com.airbnb.mvrx.MavericksState import com.puzzle.designsystem.R import com.puzzle.domain.model.profile.Contact +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.model.ValuePickRegisterRO @@ -31,7 +32,12 @@ data class RegisterProfileState( val isSmokeInputState: InputState = InputState.DEFAULT, val isSnsActive: Boolean? = null, val isSnsActiveInputState: InputState = InputState.DEFAULT, - val contacts: List = emptyList(), + val contacts: List = listOf( + Contact( + type = ContactType.KAKAO_TALK_ID, + content = "", + ) + ), val contactsInputState: InputState = InputState.DEFAULT, val valuePicks: List = emptyList(), val valueTalks: List = emptyList(), @@ -150,4 +156,4 @@ data class RegisterProfileState( const val PAGE_TRANSITION_DURATION = 1000 } } -} \ No newline at end of file +} 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 e2262931..f56ef88b 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 @@ -5,8 +5,11 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.shrinkOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -41,6 +44,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.text.isDigitsOnly import coil3.compose.AsyncImage +import com.puzzle.common.ui.clickable import com.puzzle.common.ui.throttledClickable import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceChip @@ -77,12 +81,11 @@ internal fun BasicProfilePage( val scrollState = rememberScrollState() val focusManager = LocalFocusManager.current - Column(modifier = Modifier - .padding(horizontal = 20.dp) - .verticalScroll(scrollState) - .clickable { - focusManager.clearFocus() - } + Column( + modifier = Modifier + .padding(horizontal = 20.dp) + .verticalScroll(scrollState) + .clickable { focusManager.clearFocus() }, ) { Text( text = stringResource(R.string.basic_profile_page_header), @@ -210,7 +213,6 @@ internal fun BasicProfilePage( focusManager.clearFocus() onAddContactClick() }, - modifier = Modifier.fillMaxWidth() ) } } @@ -224,7 +226,6 @@ private fun ColumnScope.SnsPlatformContent( onSnsPlatformChange: (Int) -> Unit, onDeleteClick: (Int) -> Unit, onAddContactClick: () -> Unit, - modifier: Modifier = Modifier, ) { val isSaveFailed: Boolean = contactsInputState == InputState.WARNIING @@ -253,6 +254,8 @@ private fun ColumnScope.SnsPlatformContent( AnimatedVisibility( visible = contacts.size < 4, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), modifier = Modifier.fillMaxWidth(), ) { Column { @@ -324,7 +327,9 @@ private fun SnsActivityContent( } AnimatedVisibility( - visible = isSaveFailed + visible = isSaveFailed, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), ) { Text( text = stringResource(R.string.basic_profile_required_field), @@ -369,7 +374,9 @@ private fun SmokeContent( } AnimatedVisibility( - visible = isSaveFailed + visible = isSaveFailed, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), ) { Text( text = stringResource(R.string.basic_profile_required_field), @@ -401,7 +408,9 @@ private fun JobContent( ) AnimatedVisibility( - visible = isSaveFailed + visible = isSaveFailed, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), ) { Text( text = stringResource(R.string.basic_profile_required_field), @@ -451,7 +460,9 @@ private fun WeightContent( } AnimatedVisibility( - visible = !errorMessage.isNullOrBlank() + visible = !errorMessage.isNullOrBlank(), + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), ) { errorMessage?.let { message -> Text( @@ -504,7 +515,9 @@ private fun HeightContent( } AnimatedVisibility( - visible = !errorMessage.isNullOrBlank() + visible = !errorMessage.isNullOrBlank(), + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), ) { errorMessage?.let { message -> Text( @@ -539,7 +552,9 @@ private fun LocationContent( ) AnimatedVisibility( - visible = isSaveFailed + visible = isSaveFailed, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), ) { Text( text = stringResource(R.string.basic_profile_required_field), @@ -586,7 +601,11 @@ private fun BirthdateContent( modifier = modifier.onFocusChanged { isInputFocused = it.isFocused }, ) - AnimatedVisibility(visible = isGuideMessageVisible) { + AnimatedVisibility( + visible = isGuideMessageVisible, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), + ) { Text( text = if (isSaveFailed) { stringResource(R.string.basic_profile_required_field) @@ -642,7 +661,11 @@ private fun SelfDescriptionContent( modifier = modifier.onFocusChanged { isInputFocused = it.isFocused }, ) - AnimatedVisibility(visible = isGuidanceVisible) { + AnimatedVisibility( + visible = isGuidanceVisible, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), + ) { Row( modifier = Modifier .padding(top = 8.dp) @@ -749,7 +772,11 @@ private fun NickNameContent( ) } - AnimatedVisibility(visible = isGuideMessageVisible) { + AnimatedVisibility( + visible = isGuideMessageVisible, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), + ) { Row( modifier = Modifier .padding(top = 8.dp) @@ -839,7 +866,9 @@ private fun PhotoContent( } AnimatedVisibility( - visible = isSaveFailed + visible = isSaveFailed, + enter = fadeIn() + slideInVertically(), + exit = shrinkOut() + slideOutVertically(), ) { Text( text = stringResource(R.string.basic_profile_required_field), diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/FinishPage.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/FinishPage.kt index 9c38c589..aa3e76d2 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/FinishPage.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/FinishPage.kt @@ -54,9 +54,9 @@ internal fun FinishPage( colorFilter = ColorFilter.tint(PieceTheme.colors.dark3), modifier = Modifier .align(Alignment.CenterHorizontally) - .size(300.dp) .padding(horizontal = 37.dp) - .padding(top = 92.dp), + .padding(top = 92.dp) + .size(300.dp), ) Spacer( diff --git a/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/ValueTalkPage.kt b/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/ValueTalkPage.kt index f2524d00..b274187c 100644 --- a/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/ValueTalkPage.kt +++ b/feature/profile/src/main/java/com/puzzle/profile/graph/register/page/ValueTalkPage.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.puzzle.designsystem.R @@ -222,6 +223,8 @@ fun GuideRow( Text( text = message, style = PieceTheme.typography.bodySR, + maxLines = 1, + overflow = TextOverflow.Ellipsis, color = PieceTheme.colors.dark2, modifier = Modifier .height(rowHeightDp) 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 fb23ae68..f988873a 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 @@ -60,7 +60,7 @@ private fun ValuePickScreen( onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { - var valuePicks: List by remember { mutableStateOf(state.valuePicks) } + var valuePicks: List by remember(state.valuePicks) { mutableStateOf(state.valuePicks) } var isContentEdited: Boolean by remember { mutableStateOf(false) } BackHandler { onBackClick() } @@ -110,10 +110,7 @@ private fun ValuePickScreen( }, modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 20.dp, - vertical = 14.dp, - ), + .padding(horizontal = 20.dp), ) ValuePickCards( @@ -178,8 +175,8 @@ private fun ValuePickCard( contentDescription = "질문", colorFilter = ColorFilter.tint(PieceTheme.colors.primaryDefault), modifier = Modifier + .padding(start = 4.dp) .size(20.dp) - .padding(start = 4.dp), ) Text( 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 a2465a29..f876e92b 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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -33,12 +34,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel +import com.puzzle.common.ui.addFocusCleaner import com.puzzle.common.ui.clickable import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceSubTopBar @@ -58,13 +65,27 @@ internal fun ValueTalkRoute( viewModel: ValueTalkViewModel = mavericksViewModel(), ) { val state by viewModel.collectAsState() + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + viewModel.connectSse() + } else if (event == Lifecycle.Event.ON_STOP) { + viewModel.disconnectSse() + } + } + + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } ValueTalkScreen( state = state, onBackClick = { viewModel.onIntent(ValueTalkIntent.OnBackClick) }, - onEditClick = {}, - onSaveClick = {}, - onAiSummarySaveClick = {}, + onEditClick = { viewModel.onIntent(ValueTalkIntent.OnEditClick) }, + onSaveClick = { viewModel.onIntent(ValueTalkIntent.OnUpdateClick(it)) }, + onAiSummarySaveClick = { viewModel.onIntent(ValueTalkIntent.OnAiSummarySaveClick(it)) }, ) } @@ -77,7 +98,8 @@ private fun ValueTalkScreen( onAiSummarySaveClick: (MyValueTalk) -> Unit, modifier: Modifier = Modifier, ) { - var valueTalks: List by remember { mutableStateOf(state.valueTalks) } + val focusManager = LocalFocusManager.current + var valueTalks: List by remember(state.valueTalks) { mutableStateOf(state.valueTalks) } var isContentEdited: Boolean by remember { mutableStateOf(false) } var editedValueTalkIds: List by remember { mutableStateOf(emptyList()) } @@ -92,7 +114,8 @@ private fun ValueTalkScreen( Column( modifier = modifier .fillMaxSize() - .background(PieceTheme.colors.white), + .background(PieceTheme.colors.white) + .addFocusCleaner(focusManager), ) { PieceSubTopBar( title = when (state.screenState) { @@ -135,7 +158,7 @@ private fun ValueTalkScreen( }, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 14.dp), + .padding(horizontal = 20.dp), ) ValueTalkCards( @@ -166,7 +189,10 @@ private fun ValueTalkCards( modifier: Modifier = Modifier, ) { LazyColumn(modifier = modifier.fillMaxSize()) { - itemsIndexed(valueTalks) { idx, item -> + itemsIndexed( + items = valueTalks, + key = { _, item -> item.id to item.summary }, + ) { idx, item -> ValueTalkCard( item = item, screenState = screenState, @@ -217,6 +243,7 @@ private fun ValueTalkCard( ScreenState.NORMAL -> true ScreenState.EDITING -> false }, + modifier = Modifier.fillMaxWidth(), ) when (screenState) { @@ -245,8 +272,8 @@ private fun AiSummaryContent( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp), + .padding(top = 20.dp) + .fillMaxWidth(), ) { Text( text = stringResource(R.string.value_talk_profile_aisummary_title), @@ -259,24 +286,18 @@ private fun AiSummaryContent( contentDescription = "정보", colorFilter = ColorFilter.tint(PieceTheme.colors.dark3), modifier = Modifier - .size(20.dp) - .padding(start = 4.dp), + .padding(start = 4.dp) + .size(20.dp), ) } PieceTextInputAI( value = editableAiSummary, - onValueChange = { - editableAiSummary = it - }, - onSaveClick = { - onAiSummarySaveClick( - item.copy(summary = it) - ) - }, + onValueChange = { editableAiSummary = it }, + onSaveClick = { onAiSummarySaveClick(item.copy(summary = it)) }, modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp), + .padding(top = 12.dp) + .fillMaxWidth(), ) } @@ -347,6 +368,8 @@ fun GuideRow( Text( text = message, style = PieceTheme.typography.bodySR, + maxLines = 1, + overflow = TextOverflow.Ellipsis, color = PieceTheme.colors.dark2, modifier = Modifier .height(rowHeightDp) 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 0473c0ae..a906a448 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 @@ -44,6 +44,32 @@ class ValueTalkViewModel @AssistedInject constructor( getMyValueTalksUseCase().onSuccess { setState { copy(valueTalks = it) } }.onFailure { errorHelper.sendError(it) } + + profileRepository.aiSummary + .collect { response -> + setState { + copy( + valueTalks = valueTalks.map { valueTalk -> + if (valueTalk.id == response.id) valueTalk.copy(summary = response.summary) + else valueTalk + }.toList() + ) + } + } + } + + internal fun connectSse() { + viewModelScope.launch { + profileRepository.connectSSE() + .onFailure { errorHelper.sendError(it) } + } + } + + internal fun disconnectSse() { + viewModelScope.launch { + profileRepository.disconnectSSE() + .onFailure { errorHelper.sendError(it) } + } } internal fun onIntent(intent: ValueTalkIntent) = viewModelScope.launch { @@ -55,6 +81,7 @@ class ValueTalkViewModel @AssistedInject constructor( ValueTalkIntent.OnBackClick -> processBackClick() ValueTalkIntent.OnEditClick -> setEditMode() is ValueTalkIntent.OnUpdateClick -> updateValueTalk(intent.newValueTalks) + is ValueTalkIntent.OnAiSummarySaveClick -> updateAiSummaryClick(intent.newValueTalk) } } @@ -74,13 +101,32 @@ class ValueTalkViewModel @AssistedInject constructor( setState { copy( screenState = ValueTalkState.ScreenState.NORMAL, - valueTalks = valueTalks + valueTalks = valueTalks.map { it.copy(summary = "") } ) } } .onFailure { errorHelper.sendError(it) } } + private fun updateAiSummaryClick(valueTalk: MyValueTalk) = viewModelScope.launch { + profileRepository.updateAiSummary( + profileTalkId = valueTalk.id, + summary = valueTalk.summary, + ).onSuccess { + setState { + val newValueTalks = valueTalks.map { + if (it.id == valueTalk.id) valueTalk + else it + } + + copy( + screenState = ValueTalkState.ScreenState.NORMAL, + valueTalks = newValueTalks, + ) + } + }.onFailure { errorHelper.sendError(it) } + } + @AssistedFactory interface Factory : AssistedViewModelFactory { override fun create(state: ValueTalkState): ValueTalkViewModel 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 9d32ca3a..f6290053 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 @@ -4,6 +4,7 @@ import com.puzzle.domain.model.profile.MyValueTalk sealed class ValueTalkIntent { data class OnUpdateClick(val newValueTalks: List) : ValueTalkIntent() + data class OnAiSummarySaveClick(val newValueTalk: MyValueTalk) : ValueTalkIntent() data object OnEditClick : ValueTalkIntent() data object OnBackClick : ValueTalkIntent() } 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 f1fc920f..dc6b00e4 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 @@ -4,7 +4,7 @@ import com.airbnb.mvrx.MavericksState import com.puzzle.domain.model.profile.MyValueTalk data class ValueTalkState( - var screenState: ScreenState = ScreenState.NORMAL, + val screenState: ScreenState = ScreenState.NORMAL, val valueTalks: List = emptyList(), ) : MavericksState { diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts index c327ae82..f6d54f67 100644 --- a/feature/setting/build.gradle.kts +++ b/feature/setting/build.gradle.kts @@ -36,3 +36,9 @@ android { buildConfig = true } } + +dependencies { + implementation(projects.core.common) + + implementation(libs.lottie.compose) +} diff --git a/feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingScreen.kt b/feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingScreen.kt index a9b44923..3fb65b6b 100644 --- a/feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingScreen.kt +++ b/feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingScreen.kt @@ -2,6 +2,7 @@ package com.puzzle.setting.graph.main import android.content.Context import android.content.pm.PackageManager +import android.provider.ContactsContract import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.shrinkOut @@ -14,7 +15,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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.rememberScrollState @@ -36,9 +36,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import com.puzzle.common.ui.clickable +import com.puzzle.common.ui.throttledClickable import com.puzzle.designsystem.R import com.puzzle.designsystem.component.PieceDialog import com.puzzle.designsystem.component.PieceDialogBottom @@ -79,6 +85,10 @@ internal fun SettingRoute( onUpdatePushNotification = { viewModel.onIntent(SettingIntent.UpdatePushNotification) }, onUpdateMatchNotification = { viewModel.onIntent(SettingIntent.UpdateMatchNotification) }, onUpdateBlockAcquaintances = { viewModel.onIntent(SettingIntent.UpdateBlockAcquaintances) }, + onRefreshClick = { + val phoneNumbers = readContactPhoneNumbers(context) + viewModel.onIntent(SettingIntent.OnRefreshClick(phoneNumbers)) + }, ) } @@ -94,6 +104,7 @@ private fun SettingScreen( onUpdatePushNotification: () -> Unit, onUpdateMatchNotification: () -> Unit, onUpdateBlockAcquaintances: () -> Unit, + onRefreshClick: () -> Unit, ) { var isLogoutDialogShow by remember { mutableStateOf(false) } @@ -131,6 +142,7 @@ private fun SettingScreen( HorizontalDivider( color = PieceTheme.colors.light2, thickness = 1.dp, + modifier = Modifier.padding(bottom = 16.dp), ) Column( @@ -139,11 +151,6 @@ private fun SettingScreen( .verticalScroll(rememberScrollState()) .padding(horizontal = 20.dp), ) { - LoginAccountBody( - oAuthProvider = state.oAuthProvider, - email = state.email, - ) - NotificationBody( isMatchingNotificationEnabled = state.isMatchingNotificationEnabled, isPushNotificationEnabled = state.isPushNotificationEnabled, @@ -154,8 +161,9 @@ private fun SettingScreen( SystemSettingBody( isContactBlocked = state.isContactBlocked, lastRefreshTime = state.lastRefreshTime, + isLoadingContactBlocked = state.isLoadingContactsBlocked, onContactBlockedCheckedChange = onUpdateBlockAcquaintances, - onRefreshClick = {}, + onRefreshClick = onRefreshClick, ) InquiryBody( @@ -180,58 +188,12 @@ private fun SettingScreen( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(top = 16.dp, bottom = 60.dp) - .clickable { onWithdrawClick() }, + .throttledClickable(2000L) { onWithdrawClick() }, ) } } } -@Composable -private fun LoginAccountBody( - oAuthProvider: OAuthProvider?, - email: String, -) { - Text( - text = stringResource(R.string.setting_logged_in_account), - style = PieceTheme.typography.bodySM, - color = PieceTheme.colors.dark2, - modifier = Modifier.padding(top = 20.dp, bottom = 8.dp), - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .padding(top = 20.dp, bottom = 8.dp), - ) { - val oAuthProviderIconId = when (oAuthProvider) { - OAuthProvider.GOOGLE -> R.drawable.ic_google_login - OAuthProvider.KAKAO -> R.drawable.ic_kakao_login - null -> null - } - - oAuthProviderIconId?.let { - Image( - painter = painterResource(it), - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - } - - Text( - text = email, - modifier = Modifier.padding(start = 8.dp), - ) - } - - HorizontalDivider( - color = PieceTheme.colors.light2, - thickness = 1.dp, - modifier = Modifier.padding(vertical = 16.dp), - ) -} - @Composable private fun NotificationBody( isMatchingNotificationEnabled: Boolean, @@ -295,6 +257,7 @@ private fun NotificationBody( private fun SystemSettingBody( isContactBlocked: Boolean, lastRefreshTime: String, + isLoadingContactBlocked: Boolean, onContactBlockedCheckedChange: () -> Unit, onRefreshClick: () -> Unit, ) { @@ -379,13 +342,26 @@ private fun SystemSettingBody( } } - Image( - painter = painterResource(R.drawable.ic_refresh), - contentDescription = "초기화", - modifier = Modifier - .padding(start = 11.dp) - .clickable { onRefreshClick() }, - ) + if (isLoadingContactBlocked) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.anim_setting_loading)) + val progress by animateLottieCompositionAsState( + composition = composition, + iterations = LottieConstants.IterateForever, + ) + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = Modifier.size(24.dp) + ) + } else { + Image( + painter = painterResource(R.drawable.ic_refresh), + contentDescription = "초기화", + modifier = Modifier + .padding(start = 11.dp) + .throttledClickable(2000L) { onRefreshClick() }, + ) + } } } @@ -547,6 +523,40 @@ private fun OthersBody(onLogoutClick: () -> Unit) { ) } +private fun getVersionInfo( + context: Context, + onError: (Exception) -> Unit, +): String? { + var version: String? = null + try { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + version = packageInfo.versionName + } catch (e: PackageManager.NameNotFoundException) { + onError(e) + } + return version +} + +private fun readContactPhoneNumbers(context: Context): List { + val phoneNumbers = mutableListOf() + + val contentResolver = context.contentResolver + val uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI + val projection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER) + + contentResolver.query(uri, projection, null, null, null)?.use { cursor -> + val numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + while (cursor.moveToNext()) { + val phoneNumber = cursor.getString(numberIndex).replace(Regex("[^0-9]"), "") + if (phoneNumber.isNotBlank()) { + phoneNumbers.add(phoneNumber) + } + } + } + + return phoneNumbers.distinct() +} + @Preview @Composable private fun PreviewSettingScreen() { @@ -570,6 +580,7 @@ private fun PreviewSettingScreen() { onUpdatePushNotification = {}, onUpdateMatchNotification = {}, onUpdateBlockAcquaintances = {}, + onRefreshClick = {}, ) } } @@ -597,17 +608,3 @@ private fun PreviewLogoutDialog() { ) } } - -private fun getVersionInfo( - context: Context, - onError: (Exception) -> Unit, -): String? { - var version: String? = null - try { - val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) - version = packageInfo.versionName - } catch (e: PackageManager.NameNotFoundException) { - onError(e) - } - return version -} diff --git a/feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingViewModel.kt b/feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingViewModel.kt index 5a1e1b49..021238f1 100644 --- a/feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingViewModel.kt +++ b/feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingViewModel.kt @@ -4,8 +4,10 @@ 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.toBlockSyncFormattedTime import com.puzzle.domain.model.error.ErrorHelper import com.puzzle.domain.repository.AuthRepository +import com.puzzle.domain.repository.MatchingRepository import com.puzzle.domain.repository.UserRepository import com.puzzle.navigation.AuthGraph import com.puzzle.navigation.NavigationEvent @@ -30,6 +32,7 @@ class SettingViewModel @AssistedInject constructor( @Assisted initialState: SettingState, private val authRepository: AuthRepository, private val userRepository: UserRepository, + private val matchingRepository: MatchingRepository, internal val navigationHelper: NavigationHelper, internal val errorHelper: ErrorHelper, ) : MavericksViewModel(initialState) { @@ -47,16 +50,26 @@ class SettingViewModel @AssistedInject constructor( } private fun initSetting() = viewModelScope.launch { - userRepository.getUserSettingInfo() - .onSuccess { - setState { - copy( - isContactBlocked = it.isAcquaintanceBlockEnabled, - isPushNotificationEnabled = it.isNotificationEnabled, - isMatchingNotificationEnabled = it.isMatchNotificationEnabled, - ) + launch { + userRepository.getUserSettingInfo() + .onSuccess { + setState { + copy( + isContactBlocked = it.isAcquaintanceBlockEnabled, + isPushNotificationEnabled = it.isNotificationEnabled, + isMatchingNotificationEnabled = it.isMatchNotificationEnabled, + ) + } } - } + .onFailure { errorHelper.sendError(it) } + } + + syncBlockTime() + } + + private fun syncBlockTime() = viewModelScope.launch { + userRepository.getBlockSyncTime() + .onSuccess { setState { copy(lastRefreshTime = it.toBlockSyncFormattedTime()) } } .onFailure { errorHelper.sendError(it) } } @@ -91,6 +104,7 @@ class SettingViewModel @AssistedInject constructor( SettingIntent.UpdateBlockAcquaintances -> updateBlockAcquaintances() SettingIntent.UpdateMatchNotification -> updateMatchNotification() SettingIntent.UpdatePushNotification -> updatePushNotification() + is SettingIntent.OnRefreshClick -> blockContacts(intent.phoneNumbers) } } @@ -116,11 +130,23 @@ class SettingViewModel @AssistedInject constructor( private fun updateBlockAcquaintances() = withState { state -> viewModelScope.launch { userRepository.updateBlockAcquaintances(!state.isContactBlocked) - .onSuccess { setState { copy(isContactBlocked = !state.isContactBlocked) } } + .onSuccess { + setState { copy(isContactBlocked = !state.isContactBlocked) } + syncBlockTime() + } .onFailure { errorHelper.sendError(it) } } } + private fun blockContacts(phoneNumbers: List) = viewModelScope.launch { + setState { copy(isLoadingContactsBlocked = true) } + + matchingRepository.blockContacts(phoneNumbers = phoneNumbers) + .onSuccess { syncBlockTime() } + .onFailure { errorHelper.sendError(it) } + .also { setState { copy(isLoadingContactsBlocked = false) } } + } + private fun updateMatchNotification() = withState { state -> viewModelScope.launch { userRepository.updateMatchNotification(!state.isMatchingNotificationEnabled) diff --git a/feature/setting/src/main/java/com/puzzle/setting/graph/main/contract/SettingIntent.kt b/feature/setting/src/main/java/com/puzzle/setting/graph/main/contract/SettingIntent.kt index 5493b72e..8a7e4a47 100644 --- a/feature/setting/src/main/java/com/puzzle/setting/graph/main/contract/SettingIntent.kt +++ b/feature/setting/src/main/java/com/puzzle/setting/graph/main/contract/SettingIntent.kt @@ -10,4 +10,5 @@ sealed class SettingIntent { data object UpdatePushNotification : SettingIntent() data object UpdateMatchNotification : SettingIntent() data object UpdateBlockAcquaintances : SettingIntent() + data class OnRefreshClick(val phoneNumbers: List) : SettingIntent() } diff --git a/feature/setting/src/main/java/com/puzzle/setting/graph/main/contract/SettingState.kt b/feature/setting/src/main/java/com/puzzle/setting/graph/main/contract/SettingState.kt index d68c834c..9da60787 100644 --- a/feature/setting/src/main/java/com/puzzle/setting/graph/main/contract/SettingState.kt +++ b/feature/setting/src/main/java/com/puzzle/setting/graph/main/contract/SettingState.kt @@ -9,6 +9,7 @@ data class SettingState( val isMatchingNotificationEnabled: Boolean = false, val isPushNotificationEnabled: Boolean = false, val isContactBlocked: Boolean = false, - val lastRefreshTime: String = "MM월 DD일 오전 00:00", + val lastRefreshTime: String = "YYYY년 MM월 DD일 00:00", + val isLoadingContactsBlocked: Boolean = false, val version: String = "", ) : MavericksState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6700dce0..a507a397 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,8 @@ hiltNavigationCompose = "1.2.0" # https://square.github.io/okhttp/ okhttp = "4.12.0" # https://github.com/square/retrofit +okhttpEventsource = "4.1.1" +# https://github.com/square/retrofit retrofit = "2.11.0" ## Kotlin @@ -83,7 +85,6 @@ googleAuth = "21.3.0" # https://developers.kakao.com/docs/latest/ko/android/getting-started#apply-sdk kakao = "2.20.6" - # https://coil-kt.github.io/coil/#jetpack-compose coil = "3.1.0" # https://airbnb.io/lottie/#/android-compose @@ -143,8 +144,11 @@ hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-comp hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +okhttp-eventsource = { module = "com.launchdarkly:okhttp-eventsource", version.ref = "okhttpEventsource" } + retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-kotlin-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } + kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } diff --git a/presentation/src/main/java/com/puzzle/presentation/MainViewModel.kt b/presentation/src/main/java/com/puzzle/presentation/MainViewModel.kt index 02eab72a..0afb20b5 100644 --- a/presentation/src/main/java/com/puzzle/presentation/MainViewModel.kt +++ b/presentation/src/main/java/com/puzzle/presentation/MainViewModel.kt @@ -43,8 +43,7 @@ class MainViewModel @Inject constructor( init { handleError() initConfigure() -// checkRedirection() - _isInitialized.value = true + checkRedirection() } private fun handleError() = viewModelScope.launch { @@ -100,7 +99,7 @@ class MainViewModel @Inject constructor( REGISTER -> { navigationHelper.navigate( NavigationEvent.NavigateTo( - route = AuthGraphDest.VerificationRoute, + route = AuthGraphDest.SignUpRoute, popUpTo = true, ) ) @@ -117,7 +116,7 @@ class MainViewModel @Inject constructor( NONE -> navigationHelper.navigate( NavigationEvent.NavigateTo( - route = OnboardingRoute, + route = AuthGraphDest.VerificationRoute, popUpTo = true, ) ) diff --git a/presentation/src/main/java/com/puzzle/presentation/navigation/TopLevelDestinvation.kt b/presentation/src/main/java/com/puzzle/presentation/navigation/TopLevelDestinvation.kt index 97669ce0..1e51e36d 100644 --- a/presentation/src/main/java/com/puzzle/presentation/navigation/TopLevelDestinvation.kt +++ b/presentation/src/main/java/com/puzzle/presentation/navigation/TopLevelDestinvation.kt @@ -22,7 +22,7 @@ enum class TopLevelDestination( MATCHING( iconDrawableId = R.drawable.ic_profile, contentDescription = "매칭", - title = "매칭", + title = "", route = MatchingGraphDest.MatchingRoute::class, ), SETTING( 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 91580128..7504599d 100644 --- a/presentation/src/main/java/com/puzzle/presentation/ui/App.kt +++ b/presentation/src/main/java/com/puzzle/presentation/ui/App.kt @@ -3,6 +3,10 @@ package com.puzzle.presentation.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.height @@ -74,7 +78,11 @@ fun App( }, containerColor = PieceTheme.colors.white, bottomBar = { - AnimatedVisibility(currentDestination?.shouldHideBottomNavigation() == false) { + AnimatedVisibility( + visible = currentDestination?.shouldHideBottomNavigation() == false, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically(), + ) { AppBottomBar( currentDestination = currentDestination, navigateToTopLevelDestination = navigateToTopLevelDestination, @@ -82,7 +90,7 @@ fun App( } }, floatingActionButton = { - if (currentDestination?.shouldHideBottomNavigation() == false) { + AnimatedVisibility(visible = currentDestination?.shouldHideBottomNavigation() == false) { FloatingActionButton( onClick = { navigateToTopLevelDestination(MatchingGraph) }, containerColor = PieceTheme.colors.white, @@ -125,20 +133,22 @@ private fun AppBottomBar( TopLevelDestination.topLevelDestinations.forEach { topLevelRoute -> NavigationBarItem( icon = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(top = 2.dp), - ) { - Icon( - painter = painterResource(topLevelRoute.iconDrawableId), - contentDescription = topLevelRoute.contentDescription, - modifier = Modifier.size(32.dp), - ) + if (topLevelRoute != TopLevelDestination.MATCHING) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 2.dp), + ) { + Icon( + painter = painterResource(topLevelRoute.iconDrawableId), + contentDescription = topLevelRoute.contentDescription, + modifier = Modifier.size(32.dp), + ) - Text( - text = topLevelRoute.title, - style = PieceTheme.typography.captionM, - ) + Text( + text = topLevelRoute.title, + style = PieceTheme.typography.captionM, + ) + } } }, alwaysShowLabel = false,