Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PC-608] API 스키마 변경 대응 및 AI 요약 SSE 연결 #70

Merged
merged 16 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions core/common/src/main/java/com/puzzle/common/TimeUtil.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.puzzle.common

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

/**
Expand All @@ -25,3 +26,11 @@ 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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class AuthRepositoryImpl @Inject constructor(
) : AuthRepository {
override suspend fun loginOauth(
oAuthProvider: OAuthProvider,
token: String
oauthCredential: String
): Result<Unit> = suspendRunCatching {
val response = authDataSource.loginOauth(oAuthProvider, token).getOrThrow()
val response = authDataSource.loginOauth(oAuthProvider, oauthCredential).getOrThrow()

coroutineScope {
val accessTokenJob = launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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<AiSummary> =
sseClient.aiSummaryResponse.map(SseAiSummaryResponse::toDomain)

override suspend fun loadValuePickQuestions(): Result<Unit> = suspendRunCatching {
val valuePickQuestions = profileDataSource.loadValuePickQuestions()
.getOrThrow()
Expand Down Expand Up @@ -139,6 +148,22 @@ class ProfileRepositoryImpl @Inject constructor(
updatedProfileBasic
}

override suspend fun updateAiSummary(profileTalkId: Int, summary: String): Result<Unit> =
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<Boolean> =
profileDataSource.checkNickname(nickname)

Expand Down Expand Up @@ -195,4 +220,10 @@ class ProfileRepositoryImpl @Inject constructor(
userRoleJob.join()
}
}

override suspend fun connectSSE(): Result<Unit> = suspendRunCatching { sseClient.connect() }
override suspend fun disconnectSSE(): Result<Unit> = suspendRunCatching {
profileDataSource.disconnectSSE().getOrThrow()
sseClient.disconnect()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -22,6 +24,9 @@ class UserRepositoryImpl @Inject constructor(
override suspend fun getUserSettingInfo(): Result<UserSetting> =
userDataSource.getSettingsInfo().mapCatching(GetSettingInfoResponse::toDomain)

override suspend fun getBlockSyncTime(): Result<LocalDateTime> =
userDataSource.getBlockSyncTime().mapCatching(GetBlockSyncTimeResponse::toDomain)

override suspend fun updatePushNotification(toggle: Boolean): Result<Unit> =
userDataSource.updatePushNotification(toggle)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class FakeAuthDataSource : AuthDataSource {

override suspend fun loginOauth(
provider: OAuthProvider,
token: String
oauthCredential: String
): Result<LoginOauthResponse> {
return Result.success(loginResponse)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MyValuePick>): Result<GetMyValuePicksResponse> {
Expand All @@ -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<Unit> {
return Result.success(Unit)
}

override suspend fun updateMyProfileBasic(
description: String,
Expand All @@ -94,7 +100,7 @@ class FakeProfileDataSource : ProfileDataSource {
description = description,
nickname = nickname,
age = null,
birthDate = birthDate,
birthdate = birthDate,
height = height,
weight = weight,
location = location,
Expand Down Expand Up @@ -141,6 +147,10 @@ class FakeProfileDataSource : ProfileDataSource {
)
)

override suspend fun disconnectSSE(): Result<Unit> {
TODO("Not yet implemented")
}

fun setValuePicks(picks: List<ValuePickResponse>) {
valuePicks = picks
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,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, "변경된 요약")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ fun PieceTextInputAI(
modifier: Modifier = Modifier,
throttleTime: Long = 2000L,
readOnly: Boolean = true,
limit: Int = 50,
onDone: () -> Unit = {},
) {
val keyboardController = LocalSoftwareKeyboardController.current
Expand All @@ -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 = {
Expand All @@ -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()
}

Expand All @@ -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 {
Expand All @@ -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)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading