From edc052cac623db504b6cb5245bc03b58d2f48868 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Fri, 17 Jul 2026 20:06:49 +0900 Subject: [PATCH 01/11] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8(OneButtonPopup)=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A1=B0=EC=A0=95=20(#1?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컴포넌트의 title 파라미터 nullable 타입으로 변경 --- .../component/popup/OneButtonPopup.kt | 2 +- .../designsystem/component/popup/PopupCard.kt | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt index 9c424d0..138f2f0 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt @@ -19,7 +19,7 @@ import kr.co.call.designsystem.theme.CallTheme fun OneButtonPopup( modifier: Modifier = Modifier, label: String, - title: String, + title: String? = null, buttonText: String, onButtonClick: () -> Unit, description: AnnotatedString? = null, diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/PopupCard.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/PopupCard.kt index 41646ef..742f70e 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/PopupCard.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/PopupCard.kt @@ -26,7 +26,7 @@ import kr.co.call.designsystem.theme.CallTheme internal fun PopupCard( modifier: Modifier = Modifier, label: String, - title: String, + title: String?=null, description: AnnotatedString? = null, buttons: @Composable () -> Unit, ) { @@ -50,13 +50,16 @@ internal fun PopupCard( textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(8.dp)) - Text( - text = title, - style = CallTheme.typography.bodyLargeBold, - color = CallTheme.colors.black, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) + if (title != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = title, + style = CallTheme.typography.bodyLargeBold, + color = CallTheme.colors.black, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } if (description != null) { Spacer(modifier = Modifier.height(8.dp)) Text( From 7942ab78edcfd707ad28c981ffc3290c4dc567a7 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Fri, 17 Jul 2026 20:07:55 +0900 Subject: [PATCH 02/11] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8(SecondaryButton)=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#1?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컴포넌트의 버튼 아이콘(iconRes) 파라미터 추가 --- .../component/button/SecondaryButton.kt | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/button/SecondaryButton.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/button/SecondaryButton.kt index c42acab..f98c69f 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/component/button/SecondaryButton.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/component/button/SecondaryButton.kt @@ -7,10 +7,15 @@ import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -19,6 +24,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.CallFromAiTheme @@ -33,6 +40,7 @@ fun SecondaryButton( text: String, onClick: () -> Unit, enabled: Boolean = true, + iconRes: Int? = null, containerColor: Color = CallTheme.colors.mainVariant4, contentColor: Color = CallTheme.colors.white, pressedContainerColor: Color = CallTheme.colors.subPressed3, @@ -60,11 +68,22 @@ fun SecondaryButton( ), contentAlignment = Alignment.Center, ) { - Text( - text = text, - style = CallTheme.typography.bodyMediumMedium, - color = if (enabled) contentColor else contentColor.copy(alpha = 0.38f), - ) + val contentTint = if (enabled) contentColor else contentColor.copy(alpha = 0.38f) + + if (iconRes != null) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = contentTint, + modifier = Modifier.size(16.dp), + ) + Spacer(modifier = Modifier.width(6.dp)) + Text(text = text, style = CallTheme.typography.bodyMediumMedium, color = contentTint) + } + } else { + Text(text = text, style = CallTheme.typography.bodyMediumMedium, color = contentTint) + } } } From 03d6a495c692aa5c4cc07eb9f928fe189b54e610 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Fri, 17 Jul 2026 20:22:59 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC(CharacterScreen)=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=B0=8F=20MVI=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B2=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/call/data/di/RepositoryModule.kt | 7 + .../AICharacterRepositoryImpl.kt | 38 +++ .../src/main/res/drawable/ic_mypage_edit.xml | 21 ++ .../call/domain/model/mypage/AiCharacter.kt | 12 + .../repository/AICharacterRepository.kt | 9 + .../co/call/impl/component/CharacterCard.kt | 171 +++++++++++ .../impl/screen/CharacterManagementScreen.kt | 267 ++++++++++++++++++ .../viewmodel/CharacterManagementIntent.kt | 10 + .../CharacterManagementSideEffect.kt | 11 + .../viewmodel/CharacterManagementState.kt | 9 + .../viewmodel/CharacterManagementViewModel.kt | 68 +++++ 11 files changed, 623 insertions(+) create mode 100644 core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt create mode 100644 core/designsystem/src/main/res/drawable/ic_mypage_edit.xml create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/mypage/AiCharacter.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/repository/AICharacterRepository.kt create mode 100644 feature/mypage/impl/src/main/java/kr/co/call/impl/component/CharacterCard.kt create mode 100644 feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt create mode 100644 feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementIntent.kt create mode 100644 feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementSideEffect.kt create mode 100644 feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementState.kt create mode 100644 feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementViewModel.kt diff --git a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt index ec46ea1..d8defa8 100644 --- a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt @@ -4,7 +4,9 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kr.co.call.data.repositoryImpl.AICharacterRepositoryImpl import kr.co.call.data.repositoryImpl.MyPageRepositoryImpl +import kr.co.call.domain.repository.AICharacterRepository import kr.co.call.domain.repository.MyPageRepository @Module @@ -15,4 +17,9 @@ abstract class RepositoryModule { abstract fun bindMyPageRepository( impl: MyPageRepositoryImpl ): MyPageRepository + + @Binds + abstract fun bindCharacterRepository( + impl: AICharacterRepositoryImpl + ): AICharacterRepository } \ No newline at end of file diff --git a/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt b/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt new file mode 100644 index 0000000..eec930c --- /dev/null +++ b/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt @@ -0,0 +1,38 @@ +package kr.co.call.data.repositoryImpl + +import kr.co.call.domain.model.mypage.AiCharacter +import kr.co.call.domain.repository.AICharacterRepository +import javax.inject.Inject + +// TODO: API 연동 전 임시 구현체 +class AICharacterRepositoryImpl @Inject constructor() : AICharacterRepository { + + private val aiCharacters = mutableListOf( + AiCharacter( + id = "1", name = "김민준", profileImageUrl = "", isMain = true, + createdAtLabel = "2025.05.26", daysTogetherLabel = "35일 째", lastConversationLabel = "오늘 21:42", + summary = "아이스크림을 커피보다 좋아하며, 늦은 저녁 시간에 대화하는 것을 선호해요. 디자인과 여행 이야기에 관심이 많고, 반말대를 편하게 여겨요.", + ), + AiCharacter( + id = "2", name = "박동휘", profileImageUrl = "", isMain = false, + createdAtLabel = "2025.06.19", daysTogetherLabel = "11일 째", lastConversationLabel = "6/29 12:30", + summary = "운동과 액티비티를 좋아하고, 아침 시간대 대화를 선호해요.", + ), + AiCharacter( + id = "3", name = "정유나", profileImageUrl = "", isMain = false, + createdAtLabel = "2025.06.25", daysTogetherLabel = "5일 째", lastConversationLabel = "6/28 09:04", + summary = "잔잔한 음악과 독서를 좋아하고, 차분한 대화를 선호해요.", + ), + ) + + override suspend fun getCharacters(): List = aiCharacters.toList() + + override suspend fun deleteCharacter(characterId: String) { + aiCharacters.removeAll { it.id == characterId } + } + + override suspend fun canAddCharacter(): Boolean { + // TODO: 마지막 캐릭터 생성 시각 기준 24시간 경과 여부를 판단(백엔드) + return false + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/res/drawable/ic_mypage_edit.xml b/core/designsystem/src/main/res/drawable/ic_mypage_edit.xml new file mode 100644 index 0000000..04f24f7 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_mypage_edit.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/core/domain/src/main/java/kr/co/call/domain/model/mypage/AiCharacter.kt b/core/domain/src/main/java/kr/co/call/domain/model/mypage/AiCharacter.kt new file mode 100644 index 0000000..aebfcac --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/mypage/AiCharacter.kt @@ -0,0 +1,12 @@ +package kr.co.call.domain.model.mypage + +data class AiCharacter( + val id: String, + val name: String, + val profileImageUrl: String, + val isMain: Boolean, + val createdAtLabel: String, + val daysTogetherLabel: String, + val lastConversationLabel: String, + val summary: String, +) \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/repository/AICharacterRepository.kt b/core/domain/src/main/java/kr/co/call/domain/repository/AICharacterRepository.kt new file mode 100644 index 0000000..f01c4a2 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/repository/AICharacterRepository.kt @@ -0,0 +1,9 @@ +package kr.co.call.domain.repository + +import kr.co.call.domain.model.mypage.AiCharacter + +interface AICharacterRepository { + suspend fun getCharacters(): List + suspend fun deleteCharacter(characterId: String) + suspend fun canAddCharacter(): Boolean +} \ No newline at end of file diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/component/CharacterCard.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/component/CharacterCard.kt new file mode 100644 index 0000000..09898d7 --- /dev/null +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/component/CharacterCard.kt @@ -0,0 +1,171 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import kr.co.call.designsystem.R +import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.domain.model.mypage.AiCharacter + +/** AI 캐릭터 관리 목록 아이템 카드 */ +@Composable +fun CharacterCard( + aiCharacter: AiCharacter, + onChatHistoryClick: () -> Unit, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val cardShape = RoundedCornerShape(20.dp) + + Column( + modifier = modifier + .fillMaxWidth() + .clip(cardShape) + .background(CallTheme.colors.white) + .border(width = 1.dp, color = CallTheme.colors.gray200, shape =cardShape ) + .padding(horizontal = 20.dp) + .padding(top = 26.dp, bottom = 21.dp), + ) { + Row{ + Box { + // 프로필 이미지 + AsyncImage( + model = aiCharacter.profileImageUrl, + contentDescription = aiCharacter.name, + contentScale = ContentScale.Crop, + modifier = Modifier.size(60.dp).clip(CircleShape), + placeholder = painterResource(id = R.drawable.img_mypage_profile_default), + error = painterResource(id = R.drawable.img_mypage_profile_default), + ) + // 프로필 편집하기 아이콘 + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(23.dp) + .clip(CircleShape) + .background(CallTheme.colors.white), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_mypage_edit), + contentDescription = "편집하기 버튼", + tint = Color.Unspecified + ) + } + } + + Spacer(modifier = Modifier.width(11.dp)) + + Column{ + Row{ + Text(text = aiCharacter.name, style = CallTheme.typography.bodyMedium, color = CallTheme.colors.black) + + // 메인 캐릭터 뱃지 + if (aiCharacter.isMain) { + Spacer(modifier = Modifier.width(7.dp)) + MainBadge() + } + + Spacer(modifier = Modifier.weight(1f)) + + // 채팅 기록 보기 버튼 + Text( + text = "채팅 기록 보기", + style = CallTheme.typography.caption, + color = CallTheme.colors.gray400, + textDecoration = TextDecoration.Underline, + modifier = Modifier.clickable(onClick = onChatHistoryClick), + ) + } + Spacer(modifier= Modifier.height(20.dp)) + + Row(modifier = Modifier.fillMaxWidth()) { + InfoColumn(label = "생성일", value = aiCharacter.createdAtLabel, modifier = Modifier.weight(1f)) + VerticalDivider( + thickness = 1.dp, + color = CallTheme.colors.gray100, + modifier = Modifier.height(30.dp) + ) + InfoColumn(label = "함께한 지", value = aiCharacter.daysTogetherLabel, modifier = Modifier.weight(1f)) + VerticalDivider( + thickness = 1.dp, + color = CallTheme.colors.gray100, + modifier = Modifier.height(30.dp) + ) + InfoColumn(label = "마지막 대화", value = aiCharacter.lastConversationLabel, modifier = Modifier.weight(1f)) + } + } + } + Spacer(modifier = Modifier.height(24.dp)) + + SecondaryButton( + text = "캐릭터 영구 삭제", + onClick = onDeleteClick, + iconRes = R.drawable.ic_mypage_trash, + containerColor = CallTheme.colors.mainVariant1, + modifier = Modifier.height(38.dp) + ) + + } +} + +@Composable +private fun InfoColumn(label: String, value: String, modifier: Modifier = Modifier) { + Column(modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = label, style = CallTheme.typography.caption, color = CallTheme.colors.gray400) + Spacer(modifier = Modifier.height(3.dp)) + Text(text = value, style = CallTheme.typography.captionBold, color = CallTheme.colors.black) + } +} + +@Composable +private fun MainBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .clip(RoundedCornerShape(30.dp)) + .background(CallTheme.colors.mainVariant5Chat) + .padding(horizontal = 10.dp, vertical = 3.dp), + ) { + Text(text = "메인", style = CallTheme.typography.captionBold, color = CallTheme.colors.mainVariant1) + } +} + +@Preview(showBackground = true) +@Composable +private fun CharacterCardPreview() { + CallFromAiTheme { + CharacterCard( + aiCharacter = AiCharacter("1", "김민준", "", true, "2025.05.26", "35일 째", "오늘 21:42", "요약 텍스트"), + onChatHistoryClick = {}, + onDeleteClick = {}, + ) + } +} \ No newline at end of file diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt new file mode 100644 index 0000000..b658c02 --- /dev/null +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt @@ -0,0 +1,267 @@ +package kr.co.call.impl.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.component.popup.OneButtonPopup +import kr.co.call.designsystem.component.popup.TwoButtonPopup +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.domain.model.mypage.AiCharacter +import kr.co.call.impl.component.CharacterCard +import kr.co.call.impl.component.CommonTopAppBar +import kr.co.call.impl.viewmodel.CharacterManagementIntent +import kr.co.call.impl.viewmodel.CharacterManagementSideEffect +import kr.co.call.impl.viewmodel.CharacterManagementState +import kr.co.call.impl.viewmodel.CharacterManagementViewModel +import kr.co.call.impl.viewmodel.MyPageState +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect + +@Composable +fun CharacterManagementScreen( + modifier: Modifier = Modifier, + viewModel: CharacterManagementViewModel = hiltViewModel(), + onBackClick: () -> Unit, + navigateToAddCharacter: () -> Unit, +) { + val state by viewModel.collectAsState() + + var chatHistoryTarget by remember { mutableStateOf(null) } + var deleteTarget by remember { mutableStateOf(null) } + var showMainDeleteBlocked by remember { mutableStateOf(false) } + var showAddBlocked by remember { mutableStateOf(false) } + + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is CharacterManagementSideEffect.ShowChatHistorySummary -> chatHistoryTarget = sideEffect.aiCharacter + is CharacterManagementSideEffect.ShowDeleteConfirmDialog -> deleteTarget = sideEffect.aiCharacter + is CharacterManagementSideEffect.ShowMainCharacterDeleteBlocked -> showMainDeleteBlocked = true + is CharacterManagementSideEffect.ShowAddCharacterBlocked -> showAddBlocked = true + is CharacterManagementSideEffect.NavigateToAddCharacter -> navigateToAddCharacter() + } + } + + CharacterManagementScreenContent( + state = state, + onIntent = viewModel::handleIntent, + onBackClick = onBackClick, + modifier = modifier, + ) + + chatHistoryTarget?.let { character -> + OneButtonPopup( + label = "채팅 기록 보기", + description = buildAnnotatedString { append(character.summary) }, + buttonText = "확인", + onButtonClick = { chatHistoryTarget = null }, + onDismissRequest = { chatHistoryTarget = null }, + ) + } + + deleteTarget?.let { character -> + TwoButtonPopup( + label = "캐릭터 영구 삭제", + title = "${character.name}와 헤어지시겠습니까?", + description = buildAnnotatedString { append("데이터 복구 및 재사용 불가합니다.") }, + positiveText = "삭제", + negativeText = "취소", + onPositiveClick = { + viewModel.handleIntent(CharacterManagementIntent.ConfirmDeleteCharacter(character)) + deleteTarget = null + }, + onNegativeClick = { deleteTarget = null }, + onDismissRequest = { deleteTarget = null }, + ) + } + + if (showMainDeleteBlocked) { + OneButtonPopup( + label = "캐릭터 영구 삭제", + title = "현재 메인으로 선택된\n연인은 삭제가 불가능합니다.", + buttonText = "확인", + onButtonClick = { showMainDeleteBlocked = false }, + onDismissRequest = { showMainDeleteBlocked = false }, + ) + } + + if (showAddBlocked) { + OneButtonPopup( + label = "캐릭터 추가하기", + title = "아직 캐릭터를\n추가할 수 없어요", + description = buildAnnotatedString { + append("새로운 캐릭터를 만든 후 24시간이 지나면\n또 다른 캐릭터를 만들 수 있어요") + }, + buttonText = "확인", + onButtonClick = { showAddBlocked = false }, + onDismissRequest = { showAddBlocked = false }, + ) + } +} + +@Composable +private fun CharacterManagementScreenContent( + state: CharacterManagementState, + onIntent: (CharacterManagementIntent) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize().background(CallTheme.colors.background), + ) { + // 상단 앱바 + CommonTopAppBar(title = "캐릭터 관리", onBackClick = onBackClick) + + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp) + .padding(top = 15.dp), + ) { + // 안내 문구 + Text( + text = "생성 가능한 캐릭터 수는 최대 5개입니다.", + style = CallTheme.typography.caption, + color = CallTheme.colors.gray400, + ) + + Spacer(modifier = Modifier.height(21.dp)) + + // 캐릭터 아이템 목록 + state.aiCharacters.forEach { character -> + CharacterCard( + aiCharacter = character, + onChatHistoryClick = { onIntent(CharacterManagementIntent.ClickChatHistory(character)) }, + onDeleteClick = { onIntent(CharacterManagementIntent.ClickDeleteCharacter(character)) }, + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } + // 캐릭터 추가 버튼 + SecondaryButton( + text = "캐릭터 추가하기", + onClick = { onIntent(CharacterManagementIntent.ClickAddCharacter) }, + containerColor = CallTheme.colors.mainVariant4, + contentColor = CallTheme.colors.white, + modifier = Modifier.padding(bottom = 24.dp) + .padding(horizontal = 16.dp) + ) + } +} + +// preview용 state +private val previewState = CharacterManagementState( + aiCharacters = listOf( + AiCharacter("1", "김민준", "", true, "2025.05.26", "35일 째", "오늘 21:42", "아이스크림을 커피보다 좋아해요."), + AiCharacter("2", "박동휘", "", false, "2025.06.19", "11일 째", "6/29 12:30", "운동을 좋아해요."), + AiCharacter("3", "정유나", "", false, "2025.06.25", "5일 째", "6/28 09:04", "독서를 좋아해요."), + ), +) + +@Preview(showBackground = true) +@Composable +private fun CharacterManagementScreenPreview() { + CallFromAiTheme { + CharacterManagementScreenContent( + state = previewState, + onIntent = {}, + onBackClick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ChatHistoryPopupPreview() { + CallFromAiTheme { + CharacterManagementScreenContent( + state = previewState, + onIntent = {}, + onBackClick = {}, + ) + OneButtonPopup( + label = "채팅 기록 보기", + description = buildAnnotatedString { append("아이스티를 커피보다 좋아하며, 늦은 저녁 시간에 대화하는 것을 선호해요. 디자인과 여행 이야기에 관심이 많고, 반존대를 편안하게 느껴요. ") }, + buttonText = "확인", + onButtonClick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun DeleteConfirmPopupPreview() { + CallFromAiTheme { + CharacterManagementScreenContent( + state = previewState, + onIntent = {}, + onBackClick = {}, + ) + TwoButtonPopup( + label = "캐릭터 영구 삭제", + title = "동휘와 헤어지시겠습니까?", + description = buildAnnotatedString { append("데이터 복구 및 재사용 불가합니다.") }, + positiveText = "삭제", + negativeText = "취소", + onPositiveClick = {}, + onNegativeClick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun MainDeleteBlockedPopupPreview() { + CallFromAiTheme { + CharacterManagementScreenContent( + state = previewState, + onIntent = {}, + onBackClick = {}, + ) + OneButtonPopup( + label = "캐릭터 영구 삭제", + title = "현재 메인으로 선택된\n연인은 삭제가 불가능합니다.", + buttonText = "확인", + onButtonClick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun AddBlockedPopupPreview() { + CallFromAiTheme { + CharacterManagementScreenContent( + state = previewState, + onIntent = {}, + onBackClick = {}, + ) + OneButtonPopup( + label = "캐릭터 추가하기", + title = "아직 캐릭터를\n추가할 수 없어요", + description = buildAnnotatedString { + append("새로운 캐릭터를 만든 후 24시간이 지나면\n또 다른 캐릭터를 만들 수 있어요") + }, + buttonText = "확인", + onButtonClick = {}, + ) + } +} \ No newline at end of file diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementIntent.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementIntent.kt new file mode 100644 index 0000000..f0b51e4 --- /dev/null +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementIntent.kt @@ -0,0 +1,10 @@ +package kr.co.call.impl.viewmodel + +import kr.co.call.domain.model.mypage.AiCharacter + +sealed interface CharacterManagementIntent { + data class ClickChatHistory(val aiCharacter: AiCharacter) : CharacterManagementIntent + data class ClickDeleteCharacter(val aiCharacter: AiCharacter) : CharacterManagementIntent + data class ConfirmDeleteCharacter(val aiCharacter: AiCharacter) : CharacterManagementIntent + data object ClickAddCharacter : CharacterManagementIntent +} \ No newline at end of file diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementSideEffect.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementSideEffect.kt new file mode 100644 index 0000000..a1e51fc --- /dev/null +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementSideEffect.kt @@ -0,0 +1,11 @@ +package kr.co.call.impl.viewmodel + +import kr.co.call.domain.model.mypage.AiCharacter + +sealed interface CharacterManagementSideEffect { + data class ShowChatHistorySummary(val aiCharacter: AiCharacter) : CharacterManagementSideEffect + data class ShowDeleteConfirmDialog(val aiCharacter: AiCharacter) : CharacterManagementSideEffect + data object ShowMainCharacterDeleteBlocked : CharacterManagementSideEffect + data object ShowAddCharacterBlocked : CharacterManagementSideEffect + data object NavigateToAddCharacter : CharacterManagementSideEffect +} \ No newline at end of file diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementState.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementState.kt new file mode 100644 index 0000000..276d0a6 --- /dev/null +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementState.kt @@ -0,0 +1,9 @@ +package kr.co.call.impl.viewmodel + +import kr.co.call.domain.model.mypage.AiCharacter +import kr.co.call.domain.util.LoadStatus + +data class CharacterManagementState( + val aiCharacters: List = emptyList(), + val loadStatus: LoadStatus = LoadStatus.Idle, +) \ No newline at end of file diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementViewModel.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementViewModel.kt new file mode 100644 index 0000000..7d7c2c9 --- /dev/null +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementViewModel.kt @@ -0,0 +1,68 @@ +package kr.co.call.impl.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kr.co.call.domain.model.mypage.AiCharacter +import kr.co.call.domain.repository.AICharacterRepository +import kr.co.call.domain.util.LoadStatus +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class CharacterManagementViewModel @Inject constructor( + private val aiCharacterRepository: AICharacterRepository, +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = CharacterManagementState() + ) { + loadCharacters() + } + + fun handleIntent(userIntent: CharacterManagementIntent) { + when (userIntent) { + is CharacterManagementIntent.ClickChatHistory -> showChatHistory(userIntent.aiCharacter) + is CharacterManagementIntent.ClickDeleteCharacter -> handleDeleteClick(userIntent.aiCharacter) + is CharacterManagementIntent.ConfirmDeleteCharacter -> deleteCharacter(userIntent.aiCharacter.id) + is CharacterManagementIntent.ClickAddCharacter -> checkAddCharacter() + } + } + + private fun loadCharacters() = intent { + reduce { state.copy(loadStatus = LoadStatus.Loading) } + runCatching { aiCharacterRepository.getCharacters() } + .onSuccess { characters -> + reduce { state.copy(aiCharacters = characters, loadStatus = LoadStatus.Idle) } + } + .onFailure { e -> + reduce { state.copy(loadStatus = LoadStatus.Error(e.message ?: "캐릭터 목록 불러오기 실패")) } + } + } + + private fun showChatHistory(aiCharacter: AiCharacter) = intent { + postSideEffect(CharacterManagementSideEffect.ShowChatHistorySummary(aiCharacter)) + } + + private fun handleDeleteClick(aiCharacter: AiCharacter) = intent { + if (aiCharacter.isMain) { + postSideEffect(CharacterManagementSideEffect.ShowMainCharacterDeleteBlocked) + } else { + postSideEffect(CharacterManagementSideEffect.ShowDeleteConfirmDialog(aiCharacter)) + } + } + + private fun deleteCharacter(characterId: String) = intent { + aiCharacterRepository.deleteCharacter(characterId) + reduce { state.copy(aiCharacters = state.aiCharacters.filterNot { it.id == characterId }) } + } + + private fun checkAddCharacter() = intent { + if (aiCharacterRepository.canAddCharacter()) { + postSideEffect(CharacterManagementSideEffect.NavigateToAddCharacter) + } else { + postSideEffect(CharacterManagementSideEffect.ShowAddCharacterBlocked) + } + } +} \ No newline at end of file From 0b424744beb80fe0cc184669b7f0f884170805a5 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Mon, 20 Jul 2026 17:31:41 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=ED=99=94=EB=A9=B4(CharacterManagementScre?= =?UTF-8?q?en)=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 9 ++++++++- .../main/java/kr/co/call/api/MyPageRoute.kt | 5 ++++- .../co/call/impl/entry/MyPageEntryBuilder.kt | 18 ++++++++++++++++-- .../kr/co/call/impl/screen/MyPageScreen.kt | 3 ++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index 913f726..033849a 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay +import kr.co.call.api.CharacterManagementNavKey import kr.co.call.api.ChatRoomNavKey import kr.co.call.api.ChattingNavKey import kr.co.call.api.HomeNavKey @@ -111,7 +112,13 @@ fun AppScreen(modifier: Modifier = Modifier) { appNavigator.popBackStack() } ) - myPageEntry() + myPageEntry( + navigateToCharacterManagement = { + appNavigator.navigate(CharacterManagementNavKey) + }, + onBack = { + appNavigator.popBackStack() + }) } ) } diff --git a/feature/mypage/api/src/main/java/kr/co/call/api/MyPageRoute.kt b/feature/mypage/api/src/main/java/kr/co/call/api/MyPageRoute.kt index 1c9d8a8..8aaf004 100644 --- a/feature/mypage/api/src/main/java/kr/co/call/api/MyPageRoute.kt +++ b/feature/mypage/api/src/main/java/kr/co/call/api/MyPageRoute.kt @@ -4,4 +4,7 @@ import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable @Serializable -data object MyPageNavKey: NavKey \ No newline at end of file +data object MyPageNavKey: NavKey + +@Serializable +data object CharacterManagementNavKey: NavKey \ No newline at end of file diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/entry/MyPageEntryBuilder.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/entry/MyPageEntryBuilder.kt index 7f50efa..8b3cce5 100644 --- a/feature/mypage/impl/src/main/java/kr/co/call/impl/entry/MyPageEntryBuilder.kt +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/entry/MyPageEntryBuilder.kt @@ -2,11 +2,25 @@ package kr.co.call.impl.entry import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey +import kr.co.call.api.CharacterManagementNavKey import kr.co.call.api.MyPageNavKey +import kr.co.call.impl.screen.CharacterManagementScreen import kr.co.call.impl.screen.MyPageScreen -fun EntryProviderScope.myPageEntry() { +fun EntryProviderScope.myPageEntry( + navigateToCharacterManagement: () -> Unit, + onBack: () -> Unit = {}, +) { entry { - MyPageScreen() + MyPageScreen( + onNavigateToCharacterManagement = navigateToCharacterManagement, + ) + } + + entry { + CharacterManagementScreen( + onBackClick = onBack, + navigateToAddCharacter = { /* TODO: 캐릭터 추가 화면 연결 */ }, + ) } } diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt index 1b118d0..c14a4c7 100644 --- a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt @@ -40,6 +40,7 @@ import org.orbitmvi.orbit.compose.collectSideEffect fun MyPageScreen( modifier: Modifier = Modifier, viewModel: MyPageViewModel = hiltViewModel(), + onNavigateToCharacterManagement: () -> Unit = {}, ) { val state by viewModel.collectAsState() var showLogoutDialog by remember { mutableStateOf(false) } @@ -52,7 +53,7 @@ fun MyPageScreen( is MyPageSideEffect.NavigateToChargeTicket -> { showComingSoon = true } is MyPageSideEffect.NavigateToPurchaseTicket -> { showComingSoon = true } is MyPageSideEffect.NavigateToTicketHistory -> { showComingSoon = true } - is MyPageSideEffect.NavigateToCharacterManagement -> { /* TODO */ } + is MyPageSideEffect.NavigateToCharacterManagement -> onNavigateToCharacterManagement() is MyPageSideEffect.NavigateToFaq -> { /* TODO */ } is MyPageSideEffect.NavigateToInquiry -> { showComingSoon = true } is MyPageSideEffect.NavigateToTerms -> { /* TODO */ } From 7d1ece1dcb88fb645678194ee2aef9b9c49cf297 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Mon, 20 Jul 2026 17:48:50 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=83=81=ED=83=9C=EB=B0=94=20=ED=8C=A8?= =?UTF-8?q?=EB=94=A9=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/call/impl/screen/CharacterManagementScreen.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt index b658c02..a63a86d 100644 --- a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text @@ -124,7 +125,10 @@ private fun CharacterManagementScreenContent( modifier: Modifier = Modifier, ) { Column( - modifier = modifier.fillMaxSize().background(CallTheme.colors.background), + modifier = modifier + .fillMaxSize() + .background(CallTheme.colors.background) + .statusBarsPadding(), ) { // 상단 앱바 CommonTopAppBar(title = "캐릭터 관리", onBackClick = onBackClick) From 08ebaedb77ed786f2c1e5b93227c8806097c0933 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Wed, 22 Jul 2026 08:15:03 +0900 Subject: [PATCH 06/11] =?UTF-8?q?docs:=20CharacterManagementScreen=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/impl/screen/CharacterManagementScreen.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt index a63a86d..b0b417e 100644 --- a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt @@ -32,7 +32,6 @@ import kr.co.call.impl.viewmodel.CharacterManagementIntent import kr.co.call.impl.viewmodel.CharacterManagementSideEffect import kr.co.call.impl.viewmodel.CharacterManagementState import kr.co.call.impl.viewmodel.CharacterManagementViewModel -import kr.co.call.impl.viewmodel.MyPageState import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -67,6 +66,7 @@ fun CharacterManagementScreen( modifier = modifier, ) + // 팝업 - 채팅 기록 보기 chatHistoryTarget?.let { character -> OneButtonPopup( label = "채팅 기록 보기", @@ -77,6 +77,7 @@ fun CharacterManagementScreen( ) } + // 팝업 - 캐릭터 영구 삭제 확인 deleteTarget?.let { character -> TwoButtonPopup( label = "캐릭터 영구 삭제", @@ -93,6 +94,7 @@ fun CharacterManagementScreen( ) } + // 팝업 - 캐릭터 영구 삭제 if (showMainDeleteBlocked) { OneButtonPopup( label = "캐릭터 영구 삭제", @@ -103,6 +105,7 @@ fun CharacterManagementScreen( ) } + // 팝업 - 캐릭터 추가하기(24시간 생성 제약) if (showAddBlocked) { OneButtonPopup( label = "캐릭터 추가하기", From 50415974cc455eed1bcfdadc8d8898f7c36ba191 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Wed, 22 Jul 2026 08:16:42 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20=ED=9C=B4=EC=A7=80=ED=86=B5=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/ic_mypage_trash.xml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 core/designsystem/src/main/res/drawable/ic_mypage_trash.xml diff --git a/core/designsystem/src/main/res/drawable/ic_mypage_trash.xml b/core/designsystem/src/main/res/drawable/ic_mypage_trash.xml new file mode 100644 index 0000000..78952fd --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_mypage_trash.xml @@ -0,0 +1,25 @@ + + + + + + From 0be34232d0490af46a2abe02f9c87bd1fab4b83b Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Fri, 24 Jul 2026 09:00:45 +0900 Subject: [PATCH 08/11] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/call/data/di/RepositoryModule.kt | 2 + .../AICharacterRepositoryImpl.kt | 16 +++++--- .../call/domain/model/mypage/AiCharacter.kt | 14 +++---- .../repository/AICharacterRepository.kt | 6 +-- .../co/call/impl/component/CharacterCard.kt | 1 + .../viewmodel/CharacterManagementViewModel.kt | 37 +++++++++++++++---- 6 files changed, 54 insertions(+), 22 deletions(-) diff --git a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt index fb89828..dbbedaa 100644 --- a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt @@ -29,11 +29,13 @@ abstract class RepositoryModule { homeRepositoryImpl: HomeRepositoryImpl, ): HomeRepository @Binds + @Singleton abstract fun bindMyPageRepository( impl: MyPageRepositoryImpl ): MyPageRepository @Binds + @Singleton abstract fun bindCharacterRepository( impl: AICharacterRepositoryImpl ): AICharacterRepository diff --git a/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt b/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt index eec930c..c3fa4c4 100644 --- a/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt +++ b/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt @@ -25,14 +25,20 @@ class AICharacterRepositoryImpl @Inject constructor() : AICharacterRepository { ), ) - override suspend fun getCharacters(): List = aiCharacters.toList() + override suspend fun getCharacters(): Result>{ + return runCatching { + aiCharacters.toList() + } + } - override suspend fun deleteCharacter(characterId: String) { - aiCharacters.removeAll { it.id == characterId } + override suspend fun deleteCharacter(characterId: String): Result { + return runCatching { + aiCharacters.removeAll { it.id == characterId } + } } - override suspend fun canAddCharacter(): Boolean { + override suspend fun canAddCharacter(): Result { // TODO: 마지막 캐릭터 생성 시각 기준 24시간 경과 여부를 판단(백엔드) - return false + return runCatching { false } } } \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/model/mypage/AiCharacter.kt b/core/domain/src/main/java/kr/co/call/domain/model/mypage/AiCharacter.kt index aebfcac..ea36775 100644 --- a/core/domain/src/main/java/kr/co/call/domain/model/mypage/AiCharacter.kt +++ b/core/domain/src/main/java/kr/co/call/domain/model/mypage/AiCharacter.kt @@ -1,12 +1,12 @@ package kr.co.call.domain.model.mypage data class AiCharacter( - val id: String, - val name: String, - val profileImageUrl: String, + val id: String= "", + val name: String= "", + val profileImageUrl: String= "", val isMain: Boolean, - val createdAtLabel: String, - val daysTogetherLabel: String, - val lastConversationLabel: String, - val summary: String, + val createdAtLabel: String= "", + val daysTogetherLabel: String= "", + val lastConversationLabel: String= "", + val summary: String= "", ) \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/repository/AICharacterRepository.kt b/core/domain/src/main/java/kr/co/call/domain/repository/AICharacterRepository.kt index f01c4a2..174f969 100644 --- a/core/domain/src/main/java/kr/co/call/domain/repository/AICharacterRepository.kt +++ b/core/domain/src/main/java/kr/co/call/domain/repository/AICharacterRepository.kt @@ -3,7 +3,7 @@ package kr.co.call.domain.repository import kr.co.call.domain.model.mypage.AiCharacter interface AICharacterRepository { - suspend fun getCharacters(): List - suspend fun deleteCharacter(characterId: String) - suspend fun canAddCharacter(): Boolean + suspend fun getCharacters(): Result> + suspend fun deleteCharacter(characterId: String): Result + suspend fun canAddCharacter(): Result } \ No newline at end of file diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/component/CharacterCard.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/component/CharacterCard.kt index 09898d7..decfa5f 100644 --- a/feature/mypage/impl/src/main/java/kr/co/call/impl/component/CharacterCard.kt +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/component/CharacterCard.kt @@ -65,6 +65,7 @@ fun CharacterCard( error = painterResource(id = R.drawable.img_mypage_profile_default), ) // 프로필 편집하기 아이콘 + // TODO : 추후 캐릭터 편집 화면으로 이동 구현 Box( modifier = Modifier .align(Alignment.BottomEnd) diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementViewModel.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementViewModel.kt index 7d7c2c9..347cf06 100644 --- a/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementViewModel.kt +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/CharacterManagementViewModel.kt @@ -8,6 +8,7 @@ import kr.co.call.domain.util.LoadStatus import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container +import java.util.concurrent.CancellationException import javax.inject.Inject @HiltViewModel @@ -32,11 +33,13 @@ class CharacterManagementViewModel @Inject constructor( private fun loadCharacters() = intent { reduce { state.copy(loadStatus = LoadStatus.Loading) } - runCatching { aiCharacterRepository.getCharacters() } + + aiCharacterRepository.getCharacters() .onSuccess { characters -> reduce { state.copy(aiCharacters = characters, loadStatus = LoadStatus.Idle) } } .onFailure { e -> + if (e is CancellationException) throw e reduce { state.copy(loadStatus = LoadStatus.Error(e.message ?: "캐릭터 목록 불러오기 실패")) } } } @@ -54,15 +57,35 @@ class CharacterManagementViewModel @Inject constructor( } private fun deleteCharacter(characterId: String) = intent { + val targetCharacter = state.aiCharacters.find { it.id == characterId } + + if (targetCharacter?.isMain == true) { + postSideEffect(CharacterManagementSideEffect.ShowMainCharacterDeleteBlocked) + return@intent + } + aiCharacterRepository.deleteCharacter(characterId) - reduce { state.copy(aiCharacters = state.aiCharacters.filterNot { it.id == characterId }) } + .onSuccess { + reduce { state.copy(aiCharacters = state.aiCharacters.filterNot { it.id == characterId }) } + } + .onFailure { e -> + if (e is CancellationException) throw e + // TODO 삭제 실패 시 처리 + } } private fun checkAddCharacter() = intent { - if (aiCharacterRepository.canAddCharacter()) { - postSideEffect(CharacterManagementSideEffect.NavigateToAddCharacter) - } else { - postSideEffect(CharacterManagementSideEffect.ShowAddCharacterBlocked) - } + aiCharacterRepository.canAddCharacter() + .onSuccess { canAdd -> + if (canAdd) { + postSideEffect(CharacterManagementSideEffect.NavigateToAddCharacter) + } else { + postSideEffect(CharacterManagementSideEffect.ShowAddCharacterBlocked) + } + } + .onFailure { e -> + if (e is CancellationException) throw e + postSideEffect(CharacterManagementSideEffect.ShowAddCharacterBlocked) + } } } \ No newline at end of file From 5b0f740ac691ba2321b67fb157270a3953aa027c Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Fri, 24 Jul 2026 09:41:45 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AICharacterRepositoryImpl의 deleteCharacter Unit 반환 --- .../kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt b/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt index c3fa4c4..0a58432 100644 --- a/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt +++ b/core/data/src/main/java/kr/co/call/data/repositoryImpl/AICharacterRepositoryImpl.kt @@ -34,6 +34,7 @@ class AICharacterRepositoryImpl @Inject constructor() : AICharacterRepository { override suspend fun deleteCharacter(characterId: String): Result { return runCatching { aiCharacters.removeAll { it.id == characterId } + Unit } } From 484a24468bdd3763d0fdf56efc3ce9859f4b6223 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Fri, 24 Jul 2026 17:49:27 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 들여쓰기 적용 - PopupCard/OneButtonPopup 롤백 --- .../java/kr/co/call/callfromai/AppScreen.kt | 30 +++++++++---------- .../component/popup/OneButtonPopup.kt | 2 +- .../designsystem/component/popup/PopupCard.kt | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index 033849a..1dac9e5 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -15,7 +14,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity @@ -30,13 +28,13 @@ import kr.co.call.api.HomeNavKey import kr.co.call.api.MyPageNavKey import kr.co.call.callfromai.ui.MainBottomBar import kr.co.call.callfromai.ui.MainTab +import kr.co.call.callfromai.util.toMainTab import kr.co.call.designsystem.component.LocalBottomBarPadding import kr.co.call.impl.entry.chattingEntry import kr.co.call.impl.entry.homeEntry import kr.co.call.impl.entry.loginEntry import kr.co.call.impl.entry.myPageEntry import kr.co.call.impl.entry.onboardingEntry -import kr.co.call.callfromai.util.toMainTab /** * 애플리케이션 화면 내비게이션의 메인 진입점입니다. @@ -80,26 +78,27 @@ fun AppScreen(modifier: Modifier = Modifier) { modifier = modifier.fillMaxSize(), bottomBar = { if (showBottomBar) { - MainBottomBar( - currentTab = currentTab, - onTabSelected = appNavigator::navigateToTab, - modifier = Modifier - .onSizeChanged { bottomBarHeightPx = it.height }, - ) + MainBottomBar( + currentTab = currentTab, + onTabSelected = appNavigator::navigateToTab, + modifier = Modifier + .onSizeChanged { bottomBarHeightPx = it.height }, + ) } }, contentWindowInsets = WindowInsets.safeDrawing.only( WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal ), - ) { - padding -> + ) { padding -> CompositionLocalProvider( LocalBottomBarPadding provides if (showBottomBar) bottomBarPadding else 0.dp, ) { NavDisplay( backStack = backStack, - modifier = Modifier.fillMaxSize().padding(padding), + modifier = Modifier + .fillMaxSize() + .padding(padding), entryProvider = entryProvider { loginEntry() onboardingEntry() @@ -114,11 +113,12 @@ fun AppScreen(modifier: Modifier = Modifier) { ) myPageEntry( navigateToCharacterManagement = { - appNavigator.navigate(CharacterManagementNavKey) - }, + appNavigator.navigate(CharacterManagementNavKey) + }, onBack = { appNavigator.popBackStack() - }) + } + ) } ) } diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt index 138f2f0..b66af60 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt @@ -19,7 +19,7 @@ import kr.co.call.designsystem.theme.CallTheme fun OneButtonPopup( modifier: Modifier = Modifier, label: String, - title: String? = null, + title: String? = "", buttonText: String, onButtonClick: () -> Unit, description: AnnotatedString? = null, diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/PopupCard.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/PopupCard.kt index 742f70e..ebc32e7 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/PopupCard.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/PopupCard.kt @@ -26,7 +26,7 @@ import kr.co.call.designsystem.theme.CallTheme internal fun PopupCard( modifier: Modifier = Modifier, label: String, - title: String?=null, + title: String?, description: AnnotatedString? = null, buttons: @Composable () -> Unit, ) { From 20eb19fd15cc947811ae63426e8fbd6779b4f348 Mon Sep 17 00:00:00 2001 From: WAcAW9 Date: Mon, 27 Jul 2026 15:52:53 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - popup 컴포넌트에 mutableStateOf 원소 타입을 boolean으로 변경 - 필요한 import문 추가 - 캐릭터 추가하기팝업 "24시간" 강조 --- .../kr/co/call/data/di/RepositoryModule.kt | 2 + .../component/popup/OneButtonPopup.kt | 2 +- .../impl/screen/CharacterManagementScreen.kt | 89 +++++++++++++------ 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt index 5560acb..a1d07b0 100644 --- a/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt @@ -4,12 +4,14 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kr.co.call.data.repositoryImpl.AICharacterRepositoryImpl import javax.inject.Singleton import kr.co.call.data.repositoryImpl.CallRecordMockRepository import kr.co.call.data.repositoryImpl.FaqRepositoryImpl import kr.co.call.data.repositoryImpl.ChatRepositoryImpl import kr.co.call.data.repositoryImpl.HomeMockRepository import kr.co.call.data.repositoryImpl.MyPageRepositoryImpl +import kr.co.call.domain.repository.AICharacterRepository import kr.co.call.domain.repository.CallRecordRepository import kr.co.call.domain.repository.ChatRepository import kr.co.call.domain.repository.HomeRepository diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt index 657fa75..4634a8a 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/component/popup/OneButtonPopup.kt @@ -19,7 +19,7 @@ import kr.co.call.designsystem.theme.CallTheme fun OneButtonPopup( modifier: Modifier = Modifier, label: String, - title: String? = "", + title: String? = null, buttonText: String, onButtonClick: () -> Unit, description: AnnotatedString? = null, diff --git a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt index b0b417e..d5c3aad 100644 --- a/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt +++ b/feature/mypage/impl/src/main/java/kr/co/call/impl/screen/CharacterManagementScreen.kt @@ -16,7 +16,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle 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 androidx.hilt.navigation.compose.hiltViewModel @@ -44,15 +46,25 @@ fun CharacterManagementScreen( ) { val state by viewModel.collectAsState() - var chatHistoryTarget by remember { mutableStateOf(null) } - var deleteTarget by remember { mutableStateOf(null) } + var showChatHistory by remember { mutableStateOf(false) } + var showDeleteConfirm by remember { mutableStateOf(false) } var showMainDeleteBlocked by remember { mutableStateOf(false) } var showAddBlocked by remember { mutableStateOf(false) } + // 팝업 내용 + var chatHistoryCharacter by remember { mutableStateOf(null) } + var deleteCharacter by remember { mutableStateOf(null) } + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { - is CharacterManagementSideEffect.ShowChatHistorySummary -> chatHistoryTarget = sideEffect.aiCharacter - is CharacterManagementSideEffect.ShowDeleteConfirmDialog -> deleteTarget = sideEffect.aiCharacter + is CharacterManagementSideEffect.ShowChatHistorySummary -> { + chatHistoryCharacter = sideEffect.aiCharacter + showChatHistory = true + } + is CharacterManagementSideEffect.ShowDeleteConfirmDialog -> { + deleteCharacter = sideEffect.aiCharacter + showDeleteConfirm = true + } is CharacterManagementSideEffect.ShowMainCharacterDeleteBlocked -> showMainDeleteBlocked = true is CharacterManagementSideEffect.ShowAddCharacterBlocked -> showAddBlocked = true is CharacterManagementSideEffect.NavigateToAddCharacter -> navigateToAddCharacter() @@ -67,31 +79,37 @@ fun CharacterManagementScreen( ) // 팝업 - 채팅 기록 보기 - chatHistoryTarget?.let { character -> - OneButtonPopup( - label = "채팅 기록 보기", - description = buildAnnotatedString { append(character.summary) }, - buttonText = "확인", - onButtonClick = { chatHistoryTarget = null }, - onDismissRequest = { chatHistoryTarget = null }, - ) + if (showChatHistory) { + chatHistoryCharacter?.let { character -> + OneButtonPopup( + label = "채팅 기록 보기", + description = buildAnnotatedString { append(character.summary) }, + buttonText = "확인", + onButtonClick = { showChatHistory = false }, + onDismissRequest = { showChatHistory = false }, + ) + } } // 팝업 - 캐릭터 영구 삭제 확인 - deleteTarget?.let { character -> - TwoButtonPopup( - label = "캐릭터 영구 삭제", - title = "${character.name}와 헤어지시겠습니까?", - description = buildAnnotatedString { append("데이터 복구 및 재사용 불가합니다.") }, - positiveText = "삭제", - negativeText = "취소", - onPositiveClick = { - viewModel.handleIntent(CharacterManagementIntent.ConfirmDeleteCharacter(character)) - deleteTarget = null - }, - onNegativeClick = { deleteTarget = null }, - onDismissRequest = { deleteTarget = null }, - ) + if (showDeleteConfirm) { + deleteCharacter?.let { character -> + TwoButtonPopup( + label = "캐릭터 영구 삭제", + title = "${character.name}와 헤어지시겠습니까?", + description = buildAnnotatedString { append("데이터 복구 및 재사용 불가합니다.") }, + positiveText = "삭제", + negativeText = "취소", + onPositiveClick = { + viewModel.handleIntent( + CharacterManagementIntent.ConfirmDeleteCharacter(character) + ) + showDeleteConfirm = false + }, + onNegativeClick = { showDeleteConfirm = false }, + onDismissRequest = { showDeleteConfirm = false }, + ) + } } // 팝업 - 캐릭터 영구 삭제 @@ -111,7 +129,11 @@ fun CharacterManagementScreen( label = "캐릭터 추가하기", title = "아직 캐릭터를\n추가할 수 없어요", description = buildAnnotatedString { - append("새로운 캐릭터를 만든 후 24시간이 지나면\n또 다른 캐릭터를 만들 수 있어요") + append("새로운 캐릭터를 만든 후 ") + withStyle(SpanStyle(color = CallTheme.colors.mainVariant1)) { + append("24시간") + } + append("이 지나면\n또 다른 캐릭터를 만들 수 있어요") }, buttonText = "확인", onButtonClick = { showAddBlocked = false }, @@ -168,7 +190,8 @@ private fun CharacterManagementScreenContent( onClick = { onIntent(CharacterManagementIntent.ClickAddCharacter) }, containerColor = CallTheme.colors.mainVariant4, contentColor = CallTheme.colors.white, - modifier = Modifier.padding(bottom = 24.dp) + modifier = Modifier + .padding(bottom = 24.dp) .padding(horizontal = 16.dp) ) } @@ -209,6 +232,7 @@ private fun ChatHistoryPopupPreview() { description = buildAnnotatedString { append("아이스티를 커피보다 좋아하며, 늦은 저녁 시간에 대화하는 것을 선호해요. 디자인과 여행 이야기에 관심이 많고, 반존대를 편안하게 느껴요. ") }, buttonText = "확인", onButtonClick = {}, + onDismissRequest = {}, ) } } @@ -230,6 +254,7 @@ private fun DeleteConfirmPopupPreview() { negativeText = "취소", onPositiveClick = {}, onNegativeClick = {}, + onDismissRequest = {}, ) } } @@ -248,6 +273,7 @@ private fun MainDeleteBlockedPopupPreview() { title = "현재 메인으로 선택된\n연인은 삭제가 불가능합니다.", buttonText = "확인", onButtonClick = {}, + onDismissRequest = {}, ) } } @@ -265,10 +291,15 @@ private fun AddBlockedPopupPreview() { label = "캐릭터 추가하기", title = "아직 캐릭터를\n추가할 수 없어요", description = buildAnnotatedString { - append("새로운 캐릭터를 만든 후 24시간이 지나면\n또 다른 캐릭터를 만들 수 있어요") + append("새로운 캐릭터를 만든 후 ") + withStyle(SpanStyle(color = CallTheme.colors.mainVariant1)) { + append("24시간") + } + append("이 지나면\n또 다른 캐릭터를 만들 수 있어요") }, buttonText = "확인", onButtonClick = {}, + onDismissRequest = {}, ) } } \ No newline at end of file