Skip to content

Commit

Permalink
Merge pull request #70 from YAPP-Github/feature/tgyuu/PC-608
Browse files Browse the repository at this point in the history
[PC-608] API 스키마 변경 대응 및 AI 요약 SSE 연결
  • Loading branch information
tgyuuAn authored Feb 15, 2025
2 parents 3735ab4 + bc97bd8 commit b4dca92
Show file tree
Hide file tree
Showing 69 changed files with 4,039 additions and 389 deletions.
16 changes: 16 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,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("-", "")
}
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
18 changes: 18 additions & 0 deletions core/data/src/test/java/com/puzzle/data/fake/FakeSseClient.kt
Original file line number Diff line number Diff line change
@@ -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<SseAiSummaryResponse> = flow { }

override suspend fun connect(): Result<Unit> {
return Result.success(Unit)
}

override suspend fun disconnect(): Result<Unit> {
return Result.success(Unit)
}
}
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
@@ -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
Expand All @@ -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
Expand All @@ -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() {
Expand All @@ -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,
)
}

Expand Down Expand Up @@ -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, "변경된 요약")
}
}
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b4dca92

Please sign in to comment.