diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml deleted file mode 100644 index 4a53bee..0000000 --- a/.idea/AndroidProjectSystem.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/claudeCodeTabState.xml b/.idea/claudeCodeTabState.xml deleted file mode 100644 index a643bb0..0000000 --- a/.idea/claudeCodeTabState.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 6e69a19..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index 7a54361..0000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml deleted file mode 100644 index 91f9558..0000000 --- a/.idea/deviceManager.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index 104c42f..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index ba29fd0..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 7061a0d..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 739bc36..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml deleted file mode 100644 index c61ea33..0000000 --- a/.idea/markdown.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index aa214dd..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - diff --git a/.idea/planningMode.xml b/.idea/planningMode.xml deleted file mode 100644 index c3500d9..0000000 --- a/.idea/planningMode.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 16660f1..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 5ace414..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - 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..f04d3ec 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -26,6 +26,7 @@ import androidx.navigation3.ui.NavDisplay import kr.co.call.api.ChatRoomNavKey import kr.co.call.api.ChattingNavKey import kr.co.call.api.HomeNavKey +import kr.co.call.api.ManagerChatRoomNayKey import kr.co.call.api.MyPageNavKey import kr.co.call.callfromai.ui.MainBottomBar import kr.co.call.callfromai.ui.MainTab @@ -58,7 +59,7 @@ fun AppScreen(modifier: Modifier = Modifier) { is HomeNavKey, is ChattingNavKey, is MyPageNavKey -> true - + is ManagerChatRoomNayKey -> false else -> false } @@ -104,8 +105,11 @@ fun AppScreen(modifier: Modifier = Modifier) { onboardingEntry() homeEntry() chattingEntry( - navigateToChatRoom = { roomId, name -> - appNavigator.navigate(ChatRoomNavKey(roomId = roomId, name = name)) + navigateToChatRoom = { roomId -> + appNavigator.navigate(ChatRoomNavKey(roomId = roomId)) + }, + navigateToManagerChatRoom = { + appNavigator.navigate(ManagerChatRoomNayKey) }, onBack = { appNavigator.popBackStack() diff --git a/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt b/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt index 14ac708..35ebbc9 100644 --- a/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt +++ b/core/data/src/main/java/kr/co/call/data/di/UseCaseModule.kt @@ -1,10 +1,49 @@ package kr.co.call.data.di import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kr.co.call.domain.usecase.chatting.FirstManagerChatUseCase +import kr.co.call.domain.usecase.chatting.WantToContactManagerUseCase +import kr.co.call.domain.usecase.chatting.WantToGetCallScheduleUseCase +import kr.co.call.domain.usecase.chatting.WantToUpdatePartnerInfoUseCase +import kr.co.call.domain.usecase.chatting.WantToUpdateRecordUseCase +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object UseCaseModule { + + // ======== 매니저 채팅 관련 ========= + @Provides + @Singleton + fun provideFirstManagerChatUseCase(): FirstManagerChatUseCase { + return FirstManagerChatUseCase() + } + + @Provides + @Singleton + fun provideWantToContactManagerUseCase(): WantToContactManagerUseCase { + return WantToContactManagerUseCase() + } + + @Provides + @Singleton + fun provideWantToGetCallScheduleUseCase(): WantToGetCallScheduleUseCase { + return WantToGetCallScheduleUseCase() + } + + @Provides + @Singleton + fun provideWantToUpdatePartnerInfoUseCase(): WantToUpdatePartnerInfoUseCase { + return WantToUpdatePartnerInfoUseCase() + } + + @Provides + @Singleton + fun provideWantToUpdateRecordUseCase(): WantToUpdateRecordUseCase { + return WantToUpdateRecordUseCase() + } + } diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt index 3f49f1c..9aa0bed 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt @@ -65,6 +65,13 @@ val LandingGradient=Brush.linearGradient( ) +val ChatGradientReverse = Brush.horizontalGradient( + listOf( + Color(0xFFFFA3BD), + Color(0xFFFF789F) + ) +) + // ---------- Design System Colors ---------- @Immutable data class CallColors( @@ -103,6 +110,7 @@ data class CallColors( val mainGradient: Brush = MainGradient, val chatGradient: Brush = ChatGradient, val landingGradient: Brush=LandingGradient, + val chatGradientReverse: Brush = ChatGradientReverse, ) val LocalCallColors = staticCompositionLocalOf { CallColors() } \ No newline at end of file diff --git a/core/designsystem/src/main/res/drawable/ic_chat_telephone.xml b/core/designsystem/src/main/res/drawable/ic_chat_telephone.xml new file mode 100644 index 0000000..7612565 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_chat_telephone.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerChatItem.kt b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerChatItem.kt new file mode 100644 index 0000000..20a0126 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerChatItem.kt @@ -0,0 +1,74 @@ +package kr.co.call.domain.model.chatting + +import java.time.LocalDateTime + +sealed interface ManagerChatItem { + val id: String + val content: String + val createdAt: LocalDateTime +} + +// ======== 첫 번째 매니저 채팅 ========== +data class ManagerFirstMessage( + override val id: String, + override val content: String, + val type: ManagerFirstMessageType, + override val createdAt: LocalDateTime, +): ManagerChatItem + +enum class ManagerFirstMessageType { + NORMAL, + RELATIONSHIP // 대화를 많이 나눌수록, 더 자연스러운 관계가 생성돼요. +} + + +// ======= 전화는 언제 오나요? 에 대한 매니저 채팅 ========== +data class WhenCallMessage( + override val id: String, + override val content: String, + override val createdAt: LocalDateTime, +): ManagerChatItem + + +// ======= 상대방 정보를 변경하고 싶어요. 에 대한 매니저 채팅 ========== +data class ChangePartnerInfoMessage( + override val id: String, + override val content: String, + val type: ChangePartnerInfoMessageType, + override val createdAt: LocalDateTime, +): ManagerChatItem + +enum class ChangePartnerInfoMessageType { + NORMAL, + NEXT_CONVERSATION // 변경한 설정은 다음 대화와 전화부터 바로 반영 돼요. +} + + +// ======= 기록한 정보를 수정하고 싶어요. 에 대한 매니저 채팅 ========== +data class UpdateInfoMessage( + override val id: String, + override val content: String, + val type: UpdateInfoMessageType, + override val createdAt: LocalDateTime, +): ManagerChatItem + +enum class UpdateInfoMessageType { + NORMAL, + APPLIED_NATURALLY // 수정한 정보는 이후 대화에 자연스럽게 반영 돼요 ! +} + + +// ======= 상담원에게 문의하기 에 대한 매니저 채팅 ========== +data class AskToAgentMessage( + override val id: String, + override val content: String, + override val createdAt: LocalDateTime, +): ManagerChatItem + + +// ======== 유저 채팅 ========= +data class UserMessage( + override val id: String, + override val content: String, + override val createdAt: LocalDateTime, +): ManagerChatItem diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/FirstManagerChatUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/FirstManagerChatUseCase.kt new file mode 100644 index 0000000..7945af3 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/FirstManagerChatUseCase.kt @@ -0,0 +1,67 @@ +package kr.co.call.domain.usecase.chatting + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kr.co.call.domain.model.chatting.ManagerChatItem +import kr.co.call.domain.model.chatting.ManagerFirstMessage +import kr.co.call.domain.model.chatting.ManagerFirstMessageType +import java.time.LocalDateTime +import java.util.UUID +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +/** + * 매니저 채팅방에 처음 진입했을 때 표시할 초기 메시지 목록을 + * 순차적으로 방출하는 유스케이스입니다. + * + * 메시지의 표시 간격이나 로딩 연출은 담당하지 않으며, + * 초기에 노출해야 할 메시지 데이터만 제공합니다. + */ +class FirstManagerChatUseCase @Inject constructor() { + + /** + * 초기 매니저 메시지를 [Flow] 형태로 순차 방출합니다. + * + * @return 초기 매니저 채팅 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + ManagerFirstMessage( + id = UUID.randomUUID().toString(), + content = "반가워요! 👋🏻 전화왔어 매니저에요!", + type = ManagerFirstMessageType.NORMAL, + createdAt = now + ) + ) + + emit( + ManagerFirstMessage( + id = UUID.randomUUID().toString(), + content = "전화왔어는 AI가 먼저 연락하고,\n사용자를 기억하며 관계를 만들어가는 서비스예요.", + type = ManagerFirstMessageType.NORMAL, + createdAt = now + ) + ) + + emit( + ManagerFirstMessage( + id = UUID.randomUUID().toString(), + content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", + type = ManagerFirstMessageType.RELATIONSHIP, + createdAt = now + ) + ) + + emit( + ManagerFirstMessage( + id = UUID.randomUUID().toString(), + content = "필요한 안내는 언제든 이 채팅방에서 확인해주세요!", + type = ManagerFirstMessageType.NORMAL, + createdAt = now + ) + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToContactManagerUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToContactManagerUseCase.kt new file mode 100644 index 0000000..4f28ea1 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToContactManagerUseCase.kt @@ -0,0 +1,33 @@ +package kr.co.call.domain.usecase.chatting + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kr.co.call.domain.model.chatting.AskToAgentMessage +import kr.co.call.domain.model.chatting.ManagerChatItem +import java.time.LocalDateTime +import java.util.UUID +import javax.inject.Inject + +/** + * 사용자가 "상담원에게 문의하기" 프롬프트를 선택했을 때 + * 매니저가 제공하는 안내 메시지를 제공합니다. + */ +class WantToContactManagerUseCase @Inject constructor() { + + /** + * 상담원 문의 관련 안내 메시지를 반환합니다. + * + * @return 상담원 문의 안내 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + AskToAgentMessage( + id = UUID.randomUUID().toString(), + content = "상담원 문의하기 기능은 준비중이에요.", + createdAt = now + ) + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToGetCallScheduleUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToGetCallScheduleUseCase.kt new file mode 100644 index 0000000..82f41c8 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToGetCallScheduleUseCase.kt @@ -0,0 +1,41 @@ +package kr.co.call.domain.usecase.chatting + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kr.co.call.domain.model.chatting.ManagerChatItem +import kr.co.call.domain.model.chatting.WhenCallMessage +import java.time.LocalDateTime +import java.util.UUID +import javax.inject.Inject + +/** + * 사용자가 "전화는 언제 오나요?" 프롬프트를 선택했을 때 + * 매니저가 제공하는 전화 예정 시간 안내 메시지를 제공합니다. + */ +class WantToGetCallScheduleUseCase @Inject constructor() { + + /** + * 전화 예정 시간 관련 매니저 메시지를 반환합니다. + * + * @return 전화 일정 안내 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + WhenCallMessage( + id = UUID.randomUUID().toString(), + content = "설정한 시간대를 기준으로 먼저 전화가 와요 ☎️", + createdAt = now + ) + ) + + emit( + WhenCallMessage( + id = UUID.randomUUID().toString(), + content = "원하는 시간은 마이페이지 > 연락시간 설정에서\n언제든지 변경할 수 있어요.", + createdAt = now + ) + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdatePartnerInfoUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdatePartnerInfoUseCase.kt new file mode 100644 index 0000000..66c0071 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdatePartnerInfoUseCase.kt @@ -0,0 +1,53 @@ +package kr.co.call.domain.usecase.chatting + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kr.co.call.domain.model.chatting.ChangePartnerInfoMessage +import kr.co.call.domain.model.chatting.ChangePartnerInfoMessageType +import kr.co.call.domain.model.chatting.ManagerChatItem +import java.time.LocalDateTime +import java.util.UUID +import javax.inject.Inject + +/** + * 사용자가 "상대방 정보를 수정하고 싶어요" 프롬프트를 선택했을 때 + * 매니저가 제공하는 이상형 정보 수정 안내 메시지를 제공합니다. + */ +class WantToUpdatePartnerInfoUseCase @Inject constructor() { + + /** + * 상대방(이상형) 정보 수정 관련 안내 메시지를 반환합니다. + * + * @return 이상형 정보 수정 안내 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + ChangePartnerInfoMessage( + id = UUID.randomUUID().toString(), + content = "상대방 정보는 마이페이지 > 이상형 정보 수정에서\n변경할 수 있어요. 👤", + type = ChangePartnerInfoMessageType.NORMAL, + createdAt = now + ) + ) + + emit( + ChangePartnerInfoMessage( + id = UUID.randomUUID().toString(), + content = "이름, 성격, 관계 설정까지 언제든 수정 가능해요!", + type = ChangePartnerInfoMessageType.NORMAL, + createdAt = now + ) + ) + + emit( + ChangePartnerInfoMessage( + id = UUID.randomUUID().toString(), + content = "변경한 설정은\n다음 대화와 전화부터 바로 반영 돼요.", + type = ChangePartnerInfoMessageType.NEXT_CONVERSATION, + createdAt = now + ) + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdateRecordUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdateRecordUseCase.kt new file mode 100644 index 0000000..d9cbd68 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdateRecordUseCase.kt @@ -0,0 +1,44 @@ +package kr.co.call.domain.usecase.chatting + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kr.co.call.domain.model.chatting.ManagerChatItem +import kr.co.call.domain.model.chatting.UpdateInfoMessage +import kr.co.call.domain.model.chatting.UpdateInfoMessageType +import java.time.LocalDateTime +import java.util.UUID +import javax.inject.Inject + +/** + * 사용자가 "기록한 정보를 수정하고 싶어요" 프롬프트를 선택했을 때 + * 매니저가 제공하는 내 프로필 정보 수정 안내 메시지를 제공합니다. + */ +class WantToUpdateRecordUseCase @Inject constructor() { + + /** + * 기록한 정보(내 프로필) 수정 관련 안내 메시지를 반환합니다. + * + * @return 기록 정보 수정 안내 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + UpdateInfoMessage( + id = UUID.randomUUID().toString(), + content = "기록한 정보는 마이페이지 > 내 프로필에서\n수정할 수 있어요. 📝", + type = UpdateInfoMessageType.NORMAL, + createdAt = now + ) + ) + + emit( + UpdateInfoMessage( + id = UUID.randomUUID().toString(), + content = "수정한 정보는\n이후 대화에 자연스럽게 반영 돼요!", + type = UpdateInfoMessageType.APPLIED_NATURALLY, + createdAt = now + ) + ) + } +} \ No newline at end of file diff --git a/feature/chatting/api/src/main/java/kr/co/call/api/ChattingRoute.kt b/feature/chatting/api/src/main/java/kr/co/call/api/ChattingRoute.kt index b4fe600..1c758f0 100644 --- a/feature/chatting/api/src/main/java/kr/co/call/api/ChattingRoute.kt +++ b/feature/chatting/api/src/main/java/kr/co/call/api/ChattingRoute.kt @@ -9,5 +9,7 @@ data object ChattingNavKey: NavKey @Serializable data class ChatRoomNavKey( val roomId: Long, - val name: String, -): NavKey \ No newline at end of file +): NavKey + +@Serializable +data object ManagerChatRoomNayKey: NavKey \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/BackgroundRow.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/BackgroundRow.kt similarity index 98% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/BackgroundRow.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/BackgroundRow.kt index 5f49f94..517371f 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/BackgroundRow.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/BackgroundRow.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatlist import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/ChatListItem.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/ChatListItem.kt similarity index 95% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/ChatListItem.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/ChatListItem.kt index 7448c69..1f27a26 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/ChatListItem.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/ChatListItem.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatlist import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.runtime.Composable @@ -31,7 +31,7 @@ fun ChatListItem( ) { FrontRow( chatSummary = chatSummary, - onClick = onItemClick + onClick = onItemClick ) } } diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/FrontRow.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/FrontRow.kt similarity index 99% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/FrontRow.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/FrontRow.kt index 09019c7..b2bc0f8 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/FrontRow.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/FrontRow.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatlist import androidx.compose.foundation.Image import androidx.compose.foundation.background diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/LoadingColumn.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/LoadingColumn.kt similarity index 96% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/LoadingColumn.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/LoadingColumn.kt index 4e92af1..03c4452 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/LoadingColumn.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/LoadingColumn.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatlist import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/SwipeState.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/SwipeState.kt similarity index 53% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/SwipeState.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/SwipeState.kt index 60f2943..b7a4709 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/SwipeState.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/SwipeState.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatlist enum class SwipeState { CLOSED, diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/SwipeableBox.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/SwipeableBox.kt similarity index 98% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/SwipeableBox.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/SwipeableBox.kt index 69cfbf5..eb7937d 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/SwipeableBox.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/SwipeableBox.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatlist import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/UnReadMessageCountBox.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/UnReadMessageCountBox.kt similarity index 87% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/UnReadMessageCountBox.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/UnReadMessageCountBox.kt index cb14d06..946ade7 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/UnReadMessageCountBox.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatlist/UnReadMessageCountBox.kt @@ -1,16 +1,13 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatlist import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.CallFromAiTheme diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatGrayBubble.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatGrayBubble.kt new file mode 100644 index 0000000..198c05b --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatGrayBubble.kt @@ -0,0 +1,94 @@ +package kr.co.call.impl.component.chatroom + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +@Composable +fun ChatGrayBubble( + text: AnnotatedString, + time: String, + isLoading: Boolean = false, +) { + // Modifier는 bubbleModifier 변수로 분리해 두 분기가 공유하도록 함 + val bubbleModifier = Modifier + .widthIn(max = 280.dp) + .background( + color = CallTheme.colors.gray100, + shape = RoundedCornerShape( + topStart = 0.dp, + topEnd = 20.dp, + bottomStart = 20.dp, + bottomEnd = 20.dp + ) + ) + .padding( + start = 23.dp, + end = 21.dp, + top = 9.dp, + bottom = 9.dp + ) + + Row(verticalAlignment = Alignment.Bottom) { + if (isLoading) { + LoadingIndicator(modifier = bubbleModifier) + } else { + Text( + text = text, + modifier = bubbleModifier, + style = CallTheme.typography.bodyMediumMedium.copy( + color = CallTheme.colors.black + ) + ) + } + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = time, + style = CallTheme.typography.caption.copy( + color = CallTheme.colors.gray400 + ) + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +fun ChatGrayBubblePreview() { + CallFromAiTheme { + ChatGrayBubble( + text = AnnotatedString("안녕하세요! 오늘 날씨가 참 좋네요."), + time = "12:01" + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +fun ChatGrayBubbleLoadingPreview() { + CallFromAiTheme { + ChatGrayBubble( + text = AnnotatedString(""), + time = "12:01", + isLoading = true + ) + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatPinkBubble.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatPinkBubble.kt new file mode 100644 index 0000000..46dc4f2 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatPinkBubble.kt @@ -0,0 +1,72 @@ +package kr.co.call.impl.component.chatroom + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +@Composable +fun ChatPinkBubble( + text: String, + time: String +) { + Row(verticalAlignment = Alignment.Bottom) { + Text( + text = time, + style = CallTheme.typography.caption.copy( + color = CallTheme.colors.gray400 + ) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = text, + modifier = Modifier + .widthIn(max = 280.dp) + .background( + brush = CallTheme.colors.chatGradientReverse, + shape = RoundedCornerShape( + topStart = 20.dp, + topEnd = 20.dp, + bottomStart = 20.dp, + bottomEnd = 0.dp + ) + ) + .padding( + start = 23.dp, + end = 21.dp, + top = 9.dp, + bottom = 9.dp + ), + style = CallTheme.typography.bodyMediumMedium.copy( + color = CallTheme.colors.white + ), + textAlign = TextAlign.End + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +fun ChatPinkBubblePreview() { + CallFromAiTheme { + ChatPinkBubble( + text = "오늘 뭐하고 지냈어? 보고싶다!", + time = "12:01" + ) + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/ChatRoomTopBar.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatRoomTopBar.kt similarity index 78% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/ChatRoomTopBar.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatRoomTopBar.kt index 65e1a1a..3b6c86c 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/ChatRoomTopBar.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatRoomTopBar.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatroom import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/DateSeparator.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/DateSeparator.kt new file mode 100644 index 0000000..21dbb7c --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/DateSeparator.kt @@ -0,0 +1,38 @@ +package kr.co.call.impl.component.chatroom + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +@Composable +fun DateSeparator( + text: String, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = CallTheme.typography.bodySmall, + color = CallTheme.colors.gray400 + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun DateSeparatorPreview() { + CallFromAiTheme { + DateSeparator( + text = "2026년 5월 31일" + ) + } +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/LoadingIndicator.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/LoadingIndicator.kt new file mode 100644 index 0000000..7ec39e0 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/LoadingIndicator.kt @@ -0,0 +1,132 @@ +package kr.co.call.impl.component.chatroom + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +/** + * 세 개의 점이 위아래로 튀는 애니메이션을 표시하는 컴포저블입니다. + * 채팅 인터페이스에서 "로딩 중" 상태를 나타내는 데 사용됩니다. + * + * @param modifier [Row] 레이아웃에 적용할 [Modifier]입니다. + * @param dotColors 세 개의 점에 사용할 색상 목록입니다. 기본값은 회색 계열의 그라데이션입니다. + * @param dotSize 각 점의 지름입니다. + * @param spaceBetween 점 사이의 가로 간격입니다. + * @param bounceHeight 애니메이션 동안 각 점이 위로 이동하는 최대 높이입니다. + */ +@Composable +fun LoadingIndicator( + modifier: Modifier = Modifier, + dotColors: List = listOf( + CallTheme.colors.gray200, + CallTheme.colors.gray400, + CallTheme.colors.gray600, + ), + dotSize: Dp = 8.dp, + spaceBetween: Dp = 6.dp, + bounceHeight: Dp = 3.dp, +) { + Row( + // 점이 위로 이동할 때 잘리지 않도록 상단 여백 확보 + modifier = modifier.padding(top = bounceHeight), + horizontalArrangement = Arrangement.spacedBy(spaceBetween), + ) { + repeat(3) { index -> + // 각 점마다 시작 지연 시간을 다르게 적용하여 + // 순차적으로 튀는 타이핑 인디케이터 효과 생성 + AnimatedDot( + color = dotColors[index], + size = dotSize, + bounceHeight = bounceHeight, + delayMillis = index * 150, + ) + } + } +} + +/** + * 위아래로 튀는 애니메이션을 수행하는 하나의 점을 나타냅니다. + * + * @param color 점의 색상입니다. + * @param size 점의 지름입니다. + * @param bounceHeight 점이 튀어 오를 때의 최대 세로 이동 거리입니다. + * @param delayMillis 애니메이션 시작 지연 시간(밀리초)입니다. + * 여러 점이 순차적으로 움직이는 효과를 만들기 위해 사용됩니다. + */ +@Composable +private fun AnimatedDot( + color: Color, + size: Dp, + bounceHeight: Dp, + delayMillis: Int, +) { + // 각 점마다 독립적인 무한 반복 애니메이션을 생성 + // delayMillis를 통해 점마다 애니메이션 시작 시점을 다르게 설정 + val transition = rememberInfiniteTransition(label = "typing") + + // 0 -> 1 -> 0 형태의 진행 값을 생성 + // progress 값은 실제 이동 거리 계산에 사용 + // 최고점에서는 bounceHeight만큼 위로 이동 + val progress by transition.animateFloat( + initialValue = 0f, + targetValue = 0f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 900 + // 애니메이션 시작 + 0f at 0 using FastOutSlowInEasing + + // 300ms 후 최고점 도달 + 1f at 300 using FastOutSlowInEasing + + // 600ms 후 원위치 복귀 + 0f at 600 using FastOutSlowInEasing + + // 600~900ms는 대기(idle) + }, + initialStartOffset = StartOffset(delayMillis), + ), + label = "dotOffset", + ) + + Box( + modifier = Modifier + .size(size) + // progress 값에 따라 Y축 이동 적용 + // progress가 1일 때 가장 위로 이동하고 0일 때 원위치 + .graphicsLayer { + translationY = -progress * bounceHeight.toPx() + } + .clip(CircleShape) + .background(color), + ) +} + +@Preview(showBackground = true) +@Composable +private fun LoadingIndicatorPreview() { + CallFromAiTheme { + LoadingIndicator() + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ai/gitkeep b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ai/gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubble.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubble.kt new file mode 100644 index 0000000..81cb059 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubble.kt @@ -0,0 +1,81 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +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 kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +@Composable +fun ChatLightPinkBubble( + text: AnnotatedString, + time: String +) { + Row(verticalAlignment = Alignment.Bottom) { + Box( + modifier = Modifier + .background( + color = CallTheme.colors.mainVariant5Chat, + shape = RoundedCornerShape( + topStart = 0.dp, + topEnd = 20.dp, + bottomStart = 20.dp, + bottomEnd = 20.dp + ) + ) + .padding( + start = 23.dp, + end = 21.dp, + top = 9.dp, + bottom = 9.dp + ) + ) { + Text( + text = text, + style = CallTheme.typography.bodyMediumMedium.copy( + color = CallTheme.colors.black + ) + ) + } + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = time, + style = CallTheme.typography.caption.copy( + color = CallTheme.colors.gray400 + ) + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +private fun ChatLightPinkBubblePreview() { + CallFromAiTheme { + ChatLightPinkBubble( + text = buildAnnotatedString { + append("문의를 남겨주시면\n") + withStyle(SpanStyle(color = CallTheme.colors.mainVariant1)) { + append("내용을 확인한 뒤 순차적으로 답변해 드릴게요!") + } + }, + time = "12:01" + ) + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubbleWithIcon.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubbleWithIcon.kt new file mode 100644 index 0000000..02ecf54 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubbleWithIcon.kt @@ -0,0 +1,112 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.R +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.impl.component.chatroom.LoadingIndicator + +@Composable +fun ChatLightPinkBubbleWithIcon( + text: AnnotatedString, + time: String, + isLoading: Boolean = false, +) { + // Modifier는 bubbleModifier 변수로 분리해 두 분기가 공유하도록 함 + val bubbleModifier = Modifier + .background( + color = CallTheme.colors.mainVariant5Chat, + shape = RoundedCornerShape( + topStart = 0.dp, + topEnd = 20.dp, + bottomStart = 20.dp, + bottomEnd = 20.dp + ) + ) + .padding( + start = 23.dp, + end = 21.dp, + top = 9.dp, + bottom = 9.dp + ) + + Row(verticalAlignment = Alignment.Bottom) { + if (isLoading) { + LoadingIndicator(modifier = bubbleModifier) + } else { + Row( + modifier = bubbleModifier, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_chat_telephone), + contentDescription = "전화기 이미지", + modifier = Modifier.size(width = 22.dp, height = 21.dp), + tint = CallTheme.colors.mainVariant1 + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = text, + style = CallTheme.typography.bodyMediumMedium.copy( + color = CallTheme.colors.black + ) + ) + } + } + + Spacer(modifier = Modifier.width(6.dp)) + + Text( + text = time, + style = CallTheme.typography.caption.copy( + color = CallTheme.colors.gray400 + ) + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +private fun ChatLightPinkBubblePreview() { + CallFromAiTheme { + ChatLightPinkBubbleWithIcon( + text = AnnotatedString("대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요."), + time = "12:01" + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +private fun ChatLightPinkBubbleLoadingPreview() { + CallFromAiTheme { + ChatLightPinkBubbleWithIcon( + text = AnnotatedString(""), + time = "12:01", + isLoading = true + ) + } +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatField.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatField.kt new file mode 100644 index 0000000..632a454 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatField.kt @@ -0,0 +1,55 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.shadow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallColors +import androidx.compose.ui.tooling.preview.Preview +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +@Composable +fun ManagerChatField( + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxWidth() + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(50) + ) + .height(44.dp) + .background( + color = CallTheme.colors.subGray, + shape = RoundedCornerShape(50) + ) + .padding(start = 50.dp, top = 12.dp, end = 12.dp) + ) { + Text( + text = "메시지를 입력할 수 없습니다.", + style = CallTheme.typography.bodySmall, + color = CallTheme.colors.gray600 + ) + + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +private fun ManagerChatFieldPreview() { + CallFromAiTheme { + ManagerChatField() + } +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatItemContent.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatItemContent.kt new file mode 100644 index 0000000..8af74aa --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatItemContent.kt @@ -0,0 +1,215 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import java.time.LocalDateTime +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.domain.model.chatting.AskToAgentMessage +import kr.co.call.domain.model.chatting.ChangePartnerInfoMessage +import kr.co.call.domain.model.chatting.ChangePartnerInfoMessageType +import kr.co.call.domain.model.chatting.ManagerFirstMessage +import kr.co.call.domain.model.chatting.ManagerFirstMessageType +import kr.co.call.domain.model.chatting.UpdateInfoMessage +import kr.co.call.domain.model.chatting.UpdateInfoMessageType +import kr.co.call.domain.model.chatting.UserMessage +import kr.co.call.domain.model.chatting.WhenCallMessage +import kr.co.call.domain.util.LoadStatus +import kr.co.call.impl.component.chatroom.ChatGrayBubble +import kr.co.call.impl.component.chatroom.ChatPinkBubble +import kr.co.call.impl.model.ManagerChatUiItem + +/** + * 매니저 채팅 화면에서 메시지 타입에 따른 UI 콘텐츠를 렌더링하는 컴포저블 함수입니다. + * + * [ManagerChatUiItem]의 메시지 타입을 기반으로 적절한 채팅 버블을 표시하며, + * 메시지 종류에 따라 스타일 및 강조 처리를 적용합니다. + * + * 처리하는 메시지 유형: + * - [ManagerFirstMessage]: 매니저의 첫 안내 메시지 표시 + * - 일반 메시지는 특정 키워드를 브랜드 컬러로 강조 + * - 관계 형성 안내 메시지는 아이콘이 포함된 핑크색 버블 표시 + * - [AskToAgentMessage]: 상담원 문의 요청 메시지를 핑크색 버블로 표시 + * - [ChangePartnerInfoMessage]: 상대방 정보 변경 관련 메시지 표시 + * - 변경 완료 안내 메시지는 특정 문구를 브랜드 컬러로 강조 + * - [UpdateInfoMessage]: 기록 정보 수정 관련 메시지 표시 + * - 자연스러운 반영 안내 문구를 브랜드 컬러로 강조 + * - [UserMessage]: 사용자가 보낸 메시지를 우측 정렬된 핑크색 버블로 표시 + * - [WhenCallMessage]: 전화 일정 안내 메시지를 회색 버블로 표시 + * + * @param item 렌더링할 메시지 데이터와 로딩 상태를 포함하는 UI 모델 객체 + * @param modifier 컴포저블의 레이아웃 및 동작을 제어하기 위한 Modifier + */ +@Composable +fun ManagerChatItemContent( + item: ManagerChatUiItem.Message, + modifier: Modifier = Modifier, +) { + when (val message = item.message) { + + // 첫 번째 매니저 채팅 + is ManagerFirstMessage -> { + when (message.type) { + + ManagerFirstMessageType.NORMAL -> { + ChatGrayBubble( + text = highlightText( + text = message.content, + highlightWord = "전화왔어" + ), + time = item.time, + isLoading = item.loadStatus == LoadStatus.Loading + ) + } + + ManagerFirstMessageType.RELATIONSHIP -> { + ChatLightPinkBubbleWithIcon( + text = AnnotatedString(message.content), + time = item.time, + isLoading = item.loadStatus == LoadStatus.Loading + ) + } + } + } + + // 상담원 문의 + is AskToAgentMessage -> { + ChatLightPinkBubble( + text = AnnotatedString(message.content), + time = item.time + ) + } + + // 상대방 정보 변경 + is ChangePartnerInfoMessage -> { + when (message.type) { + + ChangePartnerInfoMessageType.NORMAL -> { + ChatGrayBubble( + text = AnnotatedString(message.content), + time = item.time, + isLoading = item.loadStatus == LoadStatus.Loading + ) + } + + ChangePartnerInfoMessageType.NEXT_CONVERSATION -> { + ChatLightPinkBubble( + text = highlightText( + text = message.content, + highlightWord = "다음 대화와 전화부터 바로 반영 돼요." + ), + time = item.time + ) + } + } + } + + // 기록 정보 수정 + is UpdateInfoMessage -> { + when (message.type) { + + UpdateInfoMessageType.NORMAL -> { + ChatGrayBubble( + text = AnnotatedString(message.content), + time = item.time, + isLoading = item.loadStatus == LoadStatus.Loading + ) + } + + UpdateInfoMessageType.APPLIED_NATURALLY -> { + ChatLightPinkBubble( + text = highlightText( + text = message.content, + highlightWord = "이후 대화에 자연스럽게 반영 돼요!" + ), + time = item.time + ) + } + } + } + + // 유저 메세지 + // 오른쪽 정렬을 위해 Arrangement.End인 Row 사용 + is UserMessage -> { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 18.dp), + horizontalArrangement = Arrangement.End + ) { + ChatPinkBubble( + text = message.content, + time = item.time + ) + } + } + + // 전화 일정 안내 + is WhenCallMessage -> { + ChatGrayBubble( + text = AnnotatedString(message.content), + time = item.time, + isLoading = item.loadStatus == LoadStatus.Loading + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ManagerChatItemContentPreview() { + val sampleItems = listOf( + ManagerChatUiItem.Message( + message = ManagerFirstMessage( + id = "1", + content = "안녕하세요! 전화왔어 매니저입니다.", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + time = "오후 2:00" + ), + ManagerChatUiItem.Message( + message = ManagerFirstMessage( + id = "2", + content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", + type = ManagerFirstMessageType.RELATIONSHIP, + createdAt = LocalDateTime.now() + ), + time = "오후 2:01" + ), + ManagerChatUiItem.Message( + message = UserMessage( + id = "3", + content = "반가워요!", + createdAt = LocalDateTime.now() + ), + time = "오후 2:02" + ), + ManagerChatUiItem.Message( + message = WhenCallMessage( + id = "4", + content = "오늘 오후 3시에 전화 드릴게요.", + createdAt = LocalDateTime.now() + ), + time = "오후 2:03" + ) + ) + + CallFromAiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + sampleItems.forEach { item -> + ManagerChatItemContent(item = item) + } + } + } +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatLazyColumn.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatLazyColumn.kt new file mode 100644 index 0000000..eae0c95 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatLazyColumn.kt @@ -0,0 +1,102 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.domain.model.chatting.ManagerFirstMessage +import kr.co.call.domain.model.chatting.ManagerFirstMessageType +import kr.co.call.domain.util.LoadStatus +import kr.co.call.impl.component.chatroom.DateSeparator +import kr.co.call.impl.model.ManagerChatUiItem + +private val dateSeparatorFormatter = DateTimeFormatter.ofPattern("yyyy년 M월 d일") + +@Composable +fun ManagerChatLazyColumn( + chatItems: List, + modifier: Modifier = Modifier, + listState: LazyListState = rememberLazyListState(), + bottomPadding: Dp = 0.dp, +) { + LazyColumn( + state = listState, + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(11.dp), + contentPadding = PaddingValues( + start = 16.dp, end = 16.dp, + top = 13.dp, + bottom = 13.dp + bottomPadding, // 오버레이 높이만큼 확보 + ), + ) { + items( + items = chatItems, + key = { item -> + when (item) { + is ManagerChatUiItem.Message -> item.message.id + is ManagerChatUiItem.DateSeparator -> "date-${item.date}" + } + } + ) { item -> + when (item) { + is ManagerChatUiItem.Message -> ManagerChatItemContent(item = item) + is ManagerChatUiItem.DateSeparator -> DateSeparator( + text = item.date.format(dateSeparatorFormatter) + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ManagerChatLazyColumnPreview() { + val sampleChatItems = listOf( + ManagerChatUiItem.DateSeparator(date = LocalDate.now()), + ManagerChatUiItem.Message( + message = ManagerFirstMessage( + id = "1", + content = "안녕하세요! 무엇을 도와드릴까요?", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + time = "오후 2:00" + ), + ManagerChatUiItem.Message( + message = ManagerFirstMessage( + id = "2", + content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", + type = ManagerFirstMessageType.RELATIONSHIP, + createdAt = LocalDateTime.now() + ), + time = "오후 2:01" + ), + ManagerChatUiItem.Message( + message = ManagerFirstMessage( + id = "3", + content = "로딩 중...", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + loadStatus = LoadStatus.Loading, + time = "오후 2:02" + ) + ) + + CallFromAiTheme { + ManagerChatLazyColumn( + chatItems = sampleChatItems + ) + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatTopBar.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatTopBar.kt new file mode 100644 index 0000000..49f4522 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatTopBar.kt @@ -0,0 +1,71 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.R +import kr.co.call.designsystem.modifier.noRippleClickable +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme + +@Composable +fun ManagerChatTopBar( + modifier: Modifier = Modifier, + onBack: () -> Unit = {} +) { + Row( + modifier = modifier.background(CallTheme.colors.mainVariant5Chat) + .padding(start = 17.dp, end = 17.dp, top = 6.dp, bottom = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_chat_back), + contentDescription = "뒤로가기", + tint = Color.Unspecified, + modifier = Modifier.noRippleClickable( + onClick = onBack + ) + ) + + Spacer(Modifier.width(14.dp)) + + Image( + painter = painterResource(R.drawable.ic_chat_manager), + contentDescription = "매니저", + modifier = Modifier.clip(CircleShape) + .size(40.dp), + contentScale = ContentScale.Crop + ) + + Spacer(Modifier.width(12.dp)) + + Text( + text = "전화왔어 매니저", + style = CallTheme.typography.bodyMediumBold + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewManagerChatTopBar() { + CallFromAiTheme { + ManagerChatTopBar() + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerPromptSlide.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerPromptSlide.kt new file mode 100644 index 0000000..9f05ca9 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerPromptSlide.kt @@ -0,0 +1,146 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +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.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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.impl.intent.ManagerChatRoomIntent + +/** + * 매니저 채팅방에서 빠른 액션 프롬프트를 표시하는 드래그 가능한 슬라이드 업 패널 컴포저블입니다. + * + * 이 컴포넌트는 세로 방향 드래그 제스처를 통해 추가 옵션 영역을 확장하거나 축소할 수 있습니다. + * 상단에 드래그 핸들을 포함하며, [AnimatedVisibility]를 사용하여 + * 접힌 상태와 펼쳐진 상태 사이의 전환 애니메이션을 처리합니다. + * + * @param onIntent 사용자 상호작용에 의해 발생한 [ManagerChatRoomIntent]를 전달하는 콜백 함수입니다. + * @param modifier 이 컴포넌트의 최상위 컨테이너에 적용할 [Modifier]입니다. + */ +@Composable +fun ManagerPromptSlide( + onIntent: (ManagerChatRoomIntent) -> Unit, + modifier: Modifier = Modifier, +) { + // 슬라이드 패널의 확장 상태 관리 + var isExpanded by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .fillMaxWidth() + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(30.dp), + ) + .background(Color.White, RoundedCornerShape(30.dp)) + .draggable( + orientation = Orientation.Vertical, + // 세로 드래그 방향에 따라 패널 확장/축소 상태 변경 + // 위로 일정 거리 이상 드래그하면 확장, + // 아래로 일정 거리 이상 드래그하면 축소 + state = rememberDraggableState { delta -> + if (delta < -5f) isExpanded = true + if (delta > 5f) isExpanded = false + } + ) + .padding(top = 10.dp, bottom = 25.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + // 사용자에게 드래그 가능한 영역임을 나타내는 핸들 표시 + Box( + modifier = Modifier + .size(width = 62.dp, height = 4.dp) + .clip(CircleShape) + .background(Color(0xFFD9D2D4)) + ) + + Spacer(modifier = Modifier.height(21.dp)) + + // 기본 노출 항목 + PromptItem(emoji = "☎️", text = "전화는 언제 오나요?", onClick = { onIntent(ManagerChatRoomIntent.ClickWhenCall) }) + PromptItem(emoji = "👤", text = "상대방 정보를 변경하고 싶어요.", onClick = { onIntent(ManagerChatRoomIntent.ClickChangePartnerInfo) }) + + // 슬라이드 업 시 추가 노출 항목 + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically(animationSpec = tween(300)), + exit = shrinkVertically(animationSpec = tween(300)) + ) { + Column { + PromptItem(emoji = "📝", text = "기록한 정보를 수정하고 싶어요.", onClick = { onIntent(ManagerChatRoomIntent.ClickUpdateInfo) }) + PromptItem(emoji = "💬", text = "상담원에게 문의하기", onClick = { onIntent(ManagerChatRoomIntent.ClickAskToAgent) }) + } + } + } +} + +@Composable +private fun PromptItem( + emoji: String, + text: String, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .clickable(onClick = onClick) // 액션 항목 전체 영역을 클릭 영역으로 사용 + .padding(horizontal = 23.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 이모지 아이콘 표시 + Text( + text = emoji, + style = CallTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.width(14.dp)) + + // 액션 설명 텍스트 표시 + Text( + text = text, + style = CallTheme.typography.bodyMedium.copy( + color = CallTheme.colors.black + ) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ManagerPromptSlidePreview() { + CallFromAiTheme { + ManagerPromptSlide(onIntent = {}) + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/highlightText.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/highlightText.kt new file mode 100644 index 0000000..58573e9 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/highlightText.kt @@ -0,0 +1,48 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import kr.co.call.designsystem.theme.MainVariant1 + +/** + * 주어진 텍스트 내에서 특정 단어의 첫 번째 등장 부분을 강조 표시하는 [AnnotatedString]을 생성합니다. + * + * [highlightWord]가 [text] 내에서 발견되면, 해당 단어는 [MainVariant1] 색상으로 스타일이 적용됩니다. + * 단어를 찾지 못한 경우, 함수는 스타일이 적용되지 않은 원본 텍스트를 반환합니다. + * + * @param text 처리할 전체 문자열입니다. + */ +fun highlightText( + text: String, + highlightWord: String, +): AnnotatedString { + return buildAnnotatedString { + // 강조할 키워드의 시작 위치 + val startIndex = text.indexOf(highlightWord) + + + if (startIndex >= 0) { + // 키워드 앞부분은 기본 스타일로 추가 + append(text.substring(0, startIndex)) + + // 키워드에만 강조 색상을 적용 + withStyle( + SpanStyle(color = MainVariant1) + ) { + append(highlightWord) + } + + // 키워드 이후 문자열을 이어서 추가 + append( + text.substring( + startIndex + highlightWord.length + ) + ) + } else { + // 키워드가 없는 경우 원본 문자열을 그대로 표시 + append(text) + } + } +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/entry/ChattingEntryBuilder.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/entry/ChattingEntryBuilder.kt index 6658019..eebebd7 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/entry/ChattingEntryBuilder.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/entry/ChattingEntryBuilder.kt @@ -4,23 +4,32 @@ import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import kr.co.call.api.ChatRoomNavKey import kr.co.call.api.ChattingNavKey +import kr.co.call.api.ManagerChatRoomNayKey import kr.co.call.impl.screen.ChatListScreen import kr.co.call.impl.screen.ChatRoomScreen +import kr.co.call.impl.screen.ManagerChatRoomScreen fun EntryProviderScope.chattingEntry( - navigateToChatRoom: (Long, String) -> Unit, + navigateToChatRoom: (Long) -> Unit, + navigateToManagerChatRoom: () -> Unit, onBack: () -> Unit = {} ) { entry { ChatListScreen( - onChatRoomClick = navigateToChatRoom + onChatRoomClick = navigateToChatRoom, + onManagerChatRoomClick = navigateToManagerChatRoom ) } entry { key -> ChatRoomScreen( roomId = key.roomId, - name = key.name, + onBack = onBack + ) + } + + entry { + ManagerChatRoomScreen( onBack = onBack ) } diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ChatListIntent.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ChatListIntent.kt new file mode 100644 index 0000000..f6585fb --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ChatListIntent.kt @@ -0,0 +1,7 @@ +package kr.co.call.impl.intent + +sealed interface ChatListIntent { + data class ClickChatRoom(val roomId: Long) : ChatListIntent + + data object ClickManagerChatRoom : ChatListIntent +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ChatRoomIntent.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ChatRoomIntent.kt new file mode 100644 index 0000000..9fd1e13 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ChatRoomIntent.kt @@ -0,0 +1,4 @@ +package kr.co.call.impl.intent + +sealed interface ChatRoomIntent { +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ManagerChatRoomIntent.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ManagerChatRoomIntent.kt new file mode 100644 index 0000000..d7e11ed --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ManagerChatRoomIntent.kt @@ -0,0 +1,8 @@ +package kr.co.call.impl.intent + +sealed interface ManagerChatRoomIntent { + data object ClickWhenCall: ManagerChatRoomIntent + data object ClickChangePartnerInfo: ManagerChatRoomIntent + data object ClickUpdateInfo: ManagerChatRoomIntent + data object ClickAskToAgent: ManagerChatRoomIntent +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/mapper/UiModelMapper.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/mapper/UiModelMapper.kt index cbb9604..4775172 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/mapper/UiModelMapper.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/mapper/UiModelMapper.kt @@ -1,10 +1,13 @@ package kr.co.call.impl.mapper import kr.co.call.domain.model.chatting.ChatMessage +import kr.co.call.domain.model.chatting.ManagerChatItem import kr.co.call.domain.model.chatting.MessageType import kr.co.call.domain.model.chatting.SenderType +import kr.co.call.domain.util.LoadStatus import kr.co.call.impl.model.ChatItemUiModel import kr.co.call.impl.model.SendStatus +import kr.co.call.impl.model.ManagerChatUiItem import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.UUID @@ -12,6 +15,17 @@ import java.util.UUID private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm") object UiModelMapper { + + fun ManagerChatItem.toUiItem( + loadStatus: LoadStatus = LoadStatus.Idle, + ): ManagerChatUiItem.Message = + ManagerChatUiItem.Message( + message = this, + loadStatus = loadStatus, + time = this.createdAt.format(timeFormatter), + ) + + /** 도메인 메시지 → 말풍선 UI 모델. 항상 SENT. */ fun ChatMessage.toUiModel(): ChatItemUiModel = ChatItemUiModel( diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/model/ManagerChatUiItem.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/model/ManagerChatUiItem.kt new file mode 100644 index 0000000..d521c55 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/model/ManagerChatUiItem.kt @@ -0,0 +1,16 @@ +package kr.co.call.impl.model + +import java.time.LocalDate +import kr.co.call.domain.model.chatting.ManagerChatItem +import kr.co.call.domain.util.LoadStatus + +sealed class ManagerChatUiItem { + + data class Message( + val message: ManagerChatItem, + val loadStatus: LoadStatus = LoadStatus.Idle, + val time: String = "", + ) : ManagerChatUiItem() + + data class DateSeparator(val date: LocalDate) : ManagerChatUiItem() +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatListScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatListScreen.kt index a1bed94..3e3f754 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatListScreen.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatListScreen.kt @@ -19,12 +19,12 @@ import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme import kr.co.call.domain.model.chatting.ChatSummary import kr.co.call.domain.util.LoadStatus -import kr.co.call.impl.component.ChatListItem -import kr.co.call.impl.component.FrontRow -import kr.co.call.impl.component.LoadingColumn -import kr.co.call.impl.viewmodel.ChatListIntent -import kr.co.call.impl.viewmodel.ChatListSideEffect -import kr.co.call.impl.viewmodel.ChatListState +import kr.co.call.impl.component.chatlist.ChatListItem +import kr.co.call.impl.component.chatlist.FrontRow +import kr.co.call.impl.component.chatlist.LoadingColumn +import kr.co.call.impl.intent.ChatListIntent +import kr.co.call.impl.sideeffect.ChatListSideEffect +import kr.co.call.impl.state.ChatListState import kr.co.call.impl.viewmodel.ChatListViewModel import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -32,7 +32,8 @@ import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun ChatListScreen( modifier: Modifier = Modifier, - onChatRoomClick: (Long, String) -> Unit, + onChatRoomClick: (Long) -> Unit, + onManagerChatRoomClick: () -> Unit, viewModel: ChatListViewModel = hiltViewModel() ) { // 상태 구독 @@ -41,13 +42,13 @@ fun ChatListScreen( // 사이드이펙트 수신 viewModel.collectSideEffect { sideEffect -> when (sideEffect) { - is ChatListSideEffect.NavigateToChatRoom -> onChatRoomClick(sideEffect.roomId, sideEffect.name) + is ChatListSideEffect.NavigateToChatRoom -> onChatRoomClick(sideEffect.roomId) + ChatListSideEffect.NavigateToManagerChatRoom -> onManagerChatRoomClick() } } ChatListScreenContent( state = state, - onChatRoomClick = onChatRoomClick, onIntent = viewModel::handleIntent, modifier = modifier ) @@ -56,7 +57,6 @@ fun ChatListScreen( @Composable fun ChatListScreenContent( state: ChatListState, - onChatRoomClick: (Long, String) -> Unit, onIntent: (ChatListIntent) -> Unit, modifier: Modifier = Modifier, ) { @@ -100,8 +100,7 @@ fun ChatListScreenContent( onItemClick = { onIntent( ChatListIntent.ClickChatRoom( - roomId = chatSummary.chatRoomId, - name = chatSummary.name + roomId = chatSummary.chatRoomId ) ) } @@ -114,8 +113,9 @@ fun ChatListScreenContent( isManager = true, chatSummary = ChatSummary( name = "전화왔어 매니저", - content = "수현님, 반가워요! 👋🏻 오늘은 어떤 이야기를" - ) + content = "수현님, 반가워요! 👋🏻 오늘은 어떤 이야기를", + ), + onClick = { onIntent(ChatListIntent.ClickManagerChatRoom) } ) } } @@ -126,33 +126,33 @@ fun ChatListScreenContent( @Preview(showBackground = true) @Composable -private fun ChatListScreenPreview() { +private fun ChatListScreenContentPreview() { CallFromAiTheme { ChatListScreenContent( - onChatRoomClick = {} as (Long, String) -> Unit, - onIntent = {}, state = ChatListState( chatList = listOf( ChatSummary( chatRoomId = 1, - image = "", - name = "김철수", - isMainCharacter = false, - content = "안녕하세요! 오늘 날씨가 참 좋네요.", - whenSubmitted = "방금 전", - unReadMessageCount = "1" + name = "김민지", + isMainCharacter = true, + content = "오늘 저녁에 뭐해?", + whenSubmitted = "30분 전", + unReadMessageCount = "3", + isAlarmEnabled = true, ), ChatSummary( chatRoomId = 2, - image = "", - name = "이영희", - isMainCharacter = true, - content = "오늘 뭐해? 같이 영화 볼래?", - whenSubmitted = "30분 전", - unReadMessageCount = "5" + name = "이철수", + isMainCharacter = false, + content = "내일 봐!", + whenSubmitted = "1시간 전", + unReadMessageCount = "0", + isAlarmEnabled = false, ) - ) - ) + ), + status = LoadStatus.Idle + ), + onIntent = {} ) } } diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatRoomScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatRoomScreen.kt index c0af815..3a0290c 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatRoomScreen.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatRoomScreen.kt @@ -8,7 +8,6 @@ import kr.co.call.impl.viewmodel.ChatRoomViewModel @Composable fun ChatRoomScreen( roomId: Long, - name: String, modifier: Modifier = Modifier, //viewModel: ChatRoomViewModel = hiltViewModel(), onBack: () -> Unit = {}, diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt new file mode 100644 index 0000000..20e53ea --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt @@ -0,0 +1,183 @@ +package kr.co.call.impl.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview + +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import java.time.LocalDate +import java.time.LocalDateTime +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.domain.model.chatting.ManagerFirstMessage +import kr.co.call.domain.model.chatting.ManagerFirstMessageType +import kr.co.call.impl.component.chatroom.manager.ManagerChatField +import kr.co.call.impl.component.chatroom.manager.ManagerChatLazyColumn +import kr.co.call.impl.component.chatroom.manager.ManagerChatTopBar +import kr.co.call.impl.component.chatroom.manager.ManagerPromptSlide +import kr.co.call.impl.intent.ManagerChatRoomIntent +import kr.co.call.impl.state.ManagerChatRoomUiState +import kr.co.call.impl.viewmodel.ManagerChatRoomViewModel +import kr.co.call.impl.model.ManagerChatUiItem +import org.orbitmvi.orbit.compose.collectAsState + +@Composable +fun ManagerChatRoomScreen( + modifier: Modifier = Modifier, + onBack: () -> Unit, + viewModel: ManagerChatRoomViewModel = hiltViewModel() +) { + // 상태 구독 + val state = viewModel.collectAsState().value + + ManagerChatRoomScreenContent( + modifier = modifier, + onBack = onBack, + state = state, // 상태 호이스팅 + onIntent = viewModel::handleIntent + ) +} + +@Composable +fun ManagerChatRoomScreenContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + state: ManagerChatRoomUiState, + onIntent: (ManagerChatRoomIntent) -> Unit +) { + val density = LocalDensity.current + var overlayHeight by remember { mutableStateOf(0.dp) } + + val listState = rememberLazyListState() + + // 아이템 개수가 늘어나면 맨 아래로 + LaunchedEffect(state.chatItems.size) { + if (state.chatItems.isNotEmpty()) { + listState.animateScrollToItem(state.chatItems.lastIndex) + } + } + + Box( + modifier = modifier + .fillMaxSize() + .background(CallTheme.colors.mainVariant5Chat) + .statusBarsPadding() + ) { + Column(modifier = Modifier.fillMaxSize()) { + Spacer(Modifier.height(8.dp)) + + ManagerChatTopBar( + modifier = Modifier.fillMaxWidth(), + onBack = onBack + ) + + ManagerChatLazyColumn( + modifier = Modifier + .fillMaxSize() + .background(CallTheme.colors.background), + chatItems = state.chatItems, + bottomPadding = overlayHeight, // 측정한 높이 주입 + listState = listState, + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .onSizeChanged {// 오버레이 실제 높이 측정 + overlayHeight = with(density) { it.height.toDp() } + } + ) { + ManagerPromptSlide( + onIntent = onIntent, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(Modifier.height(15.dp)) + + ManagerChatField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + + Spacer(Modifier.height(15.dp)) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ManagerChatRoomScreenContentPreview() { + CallFromAiTheme { + ManagerChatRoomScreenContent( + state = ManagerChatRoomUiState( + chatItems = listOf( + ManagerChatUiItem.DateSeparator(date = LocalDate.now()), + ManagerChatUiItem.Message( + message = ManagerFirstMessage( + id = "1", + content = "안녕하세요! 무엇을 도와드릴까요?", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + time = "오전 10:00" + ) + ) + ), + onBack = {}, + onIntent = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ManagerChatRoomScreenContentTestPreview() { + CallFromAiTheme { + ManagerChatRoomScreenContent( + state = ManagerChatRoomUiState( + chatItems = buildList { + add(ManagerChatUiItem.DateSeparator(date = LocalDate.now())) + repeat(40) { i -> + add( + ManagerChatUiItem.Message( + message = ManagerFirstMessage( + id = "$i", // 유니크해야 함 + content = "메시지 $i - 스크롤 테스트용 더미 데이터입니다.", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + time = "오전 10:${(i % 60).toString().padStart(2, '0')}" + ) + ) + } + } + ), + onBack = {}, + onIntent = {} + ) + } +} + + diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/sideeffect/ChatListSideEffect.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/sideeffect/ChatListSideEffect.kt new file mode 100644 index 0000000..eca4cc2 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/sideeffect/ChatListSideEffect.kt @@ -0,0 +1,6 @@ +package kr.co.call.impl.sideeffect + +sealed interface ChatListSideEffect { + data class NavigateToChatRoom(val roomId: Long): ChatListSideEffect + data object NavigateToManagerChatRoom: ChatListSideEffect +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomSideEffect.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/sideeffect/ChatRoomSideEffect.kt similarity index 53% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomSideEffect.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/sideeffect/ChatRoomSideEffect.kt index d6684de..eb736ec 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomSideEffect.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/sideeffect/ChatRoomSideEffect.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.viewmodel +package kr.co.call.impl.sideeffect sealed interface ChatRoomSideEffect { diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListState.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ChatListState.kt similarity index 86% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListState.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/state/ChatListState.kt index 412d8b8..427a588 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListState.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ChatListState.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.viewmodel +package kr.co.call.impl.state import kr.co.call.domain.model.chatting.ChatSummary import kr.co.call.domain.util.LoadStatus diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomUiState.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ChatRoomUiState.kt similarity index 94% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomUiState.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/state/ChatRoomUiState.kt index e7ba581..7fbff97 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomUiState.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ChatRoomUiState.kt @@ -1,7 +1,6 @@ -package kr.co.call.impl.viewmodel +package kr.co.call.impl.state import android.net.Uri -import kr.co.call.domain.model.chatting.ChatMessageList import kr.co.call.domain.util.LoadStatus import kr.co.call.impl.model.ChatItemUiModel diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ManagerChatRoomUiState.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ManagerChatRoomUiState.kt new file mode 100644 index 0000000..b05e62a --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ManagerChatRoomUiState.kt @@ -0,0 +1,7 @@ +package kr.co.call.impl.state + +import kr.co.call.impl.model.ManagerChatUiItem + +data class ManagerChatRoomUiState( + val chatItems: List = emptyList(), +) \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListIntent.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListIntent.kt deleted file mode 100644 index 3235ce6..0000000 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListIntent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package kr.co.call.impl.viewmodel - -sealed interface ChatListIntent { - data class ClickChatRoom(val roomId: Long, val name: String) : ChatListIntent -} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListSideEffect.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListSideEffect.kt deleted file mode 100644 index 0d26ff7..0000000 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListSideEffect.kt +++ /dev/null @@ -1,5 +0,0 @@ -package kr.co.call.impl.viewmodel - -sealed interface ChatListSideEffect { - data class NavigateToChatRoom(val roomId: Long, val name: String): ChatListSideEffect -} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListViewModel.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListViewModel.kt index 4b9537a..ef43e58 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListViewModel.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListViewModel.kt @@ -4,6 +4,9 @@ import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kr.co.call.domain.repository.ChatRepository import kr.co.call.domain.util.LoadStatus +import kr.co.call.impl.intent.ChatListIntent +import kr.co.call.impl.sideeffect.ChatListSideEffect +import kr.co.call.impl.state.ChatListState import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container @@ -47,15 +50,17 @@ class ChatListViewModel @Inject constructor( fun handleIntent(intent: ChatListIntent) { when (intent) { - is ChatListIntent.ClickChatRoom -> emitNavigateToChatRoom(intent.roomId, intent.name) + is ChatListIntent.ClickChatRoom -> emitNavigateToChatRoom(intent.roomId) + ChatListIntent.ClickManagerChatRoom -> emitNavigateToManagerChatRoom() } } - private fun emitNavigateToChatRoom(roomId: Long, name: String) = intent { - postSideEffect(ChatListSideEffect.NavigateToChatRoom(roomId, name)) + private fun emitNavigateToChatRoom(roomId: Long) = intent { + postSideEffect(ChatListSideEffect.NavigateToChatRoom(roomId)) } - - + private fun emitNavigateToManagerChatRoom() = intent { + postSideEffect(ChatListSideEffect.NavigateToManagerChatRoom) + } } \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomIntent.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomIntent.kt deleted file mode 100644 index 1602d81..0000000 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomIntent.kt +++ /dev/null @@ -1,4 +0,0 @@ -package kr.co.call.impl.viewmodel - -interface ChatRoomIntent { -} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomViewModel.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomViewModel.kt index 8e40ba1..ce4c521 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomViewModel.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomViewModel.kt @@ -3,6 +3,8 @@ package kr.co.call.impl.viewmodel import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kr.co.call.domain.repository.ChatRepository +import kr.co.call.impl.sideeffect.ChatRoomSideEffect +import kr.co.call.impl.state.ChatRoomUiState import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomViewModel.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomViewModel.kt new file mode 100644 index 0000000..c8768d1 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomViewModel.kt @@ -0,0 +1,163 @@ +package kr.co.call.impl.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kr.co.call.domain.model.chatting.ManagerChatItem +import kr.co.call.domain.model.chatting.UserMessage +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.UUID +import kr.co.call.domain.usecase.chatting.FirstManagerChatUseCase +import kr.co.call.domain.usecase.chatting.WantToContactManagerUseCase +import kr.co.call.domain.usecase.chatting.WantToGetCallScheduleUseCase +import kr.co.call.domain.usecase.chatting.WantToUpdatePartnerInfoUseCase +import kr.co.call.domain.usecase.chatting.WantToUpdateRecordUseCase +import kr.co.call.domain.util.LoadStatus +import kr.co.call.impl.intent.ManagerChatRoomIntent +import kr.co.call.impl.mapper.UiModelMapper.toUiItem +import kr.co.call.impl.model.ManagerChatUiItem +import kr.co.call.impl.state.ManagerChatRoomUiState +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +@HiltViewModel +class ManagerChatRoomViewModel @Inject constructor( + // 각 유스케이스들 주입 + private val firstManagerChatUseCase: FirstManagerChatUseCase, + private val wantToGetCallScheduleUseCase: WantToGetCallScheduleUseCase, + private val wantToUpdatePartnerInfoUseCase: WantToUpdatePartnerInfoUseCase, + private val wantToUpdateRecordUseCase: WantToUpdateRecordUseCase, + private val wantToContactManagerUseCase: WantToContactManagerUseCase +): ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = ManagerChatRoomUiState() + ) + + // 용량 1의 작업 큐. 가득 차면 새로 들어온 요청을 버림(DROP_LATEST) + private val intentChannel = Channel( + capacity = 1, + onBufferOverflow = BufferOverflow.DROP_LATEST + ) + + init { + intent { + // 첫 매니저 인사 + appendManagerMessages(firstManagerChatUseCase()) + + // 채널에 전달된 Intent를 지속적으로 소비하며 순차 처리. + // 단일 코루틴이므로 직렬 처리 보장 + for (chatIntent in intentChannel) { + processIntent(chatIntent) + } + } + } + + // UI에 노출할 함수 + fun handleIntent(intent: ManagerChatRoomIntent) { + // 발생한 intent를 큐에 send + intentChannel.trySend(intent) + } + + // Intent별 사용자 액션을 처리하고, + // 해당 액션에 대응하는 사용자 메시지와 매니저 응답 Flow를 연결 + // 각 UseCase Flow에서 방출되는 메시지는 순차적으로 채팅 상태에 반영 + private suspend fun Syntax.processIntent( + intent: ManagerChatRoomIntent + ) { + when (intent) { + ManagerChatRoomIntent.ClickWhenCall -> + sendUserMessageThen("전화는 언제 오나요?", wantToGetCallScheduleUseCase()) + ManagerChatRoomIntent.ClickChangePartnerInfo -> + sendUserMessageThen("상대방 정보를 변경하고 싶어요.", wantToUpdatePartnerInfoUseCase()) + ManagerChatRoomIntent.ClickUpdateInfo -> + sendUserMessageThen("기록한 정보를 수정하고 싶어요.", wantToUpdateRecordUseCase()) + ManagerChatRoomIntent.ClickAskToAgent -> + sendUserMessageThen("상담원에게 문의하기", wantToContactManagerUseCase()) + } + } + + // 사용자의 선택 메시지를 추가한 뒤, 해당 요청에 대한 매니저 응답을 처리 + private suspend fun Syntax.sendUserMessageThen( + content: String, + flow: Flow + ) { + val userMessage = UserMessage( + id = UUID.randomUUID().toString(), + content = content, + createdAt = LocalDateTime.now() + ) + + // 유저 메세지 추가. + reduce { + state.copy( + chatItems = state.chatItems + + dateSeparatorIfNeeded(state.chatItems, userMessage.createdAt.toLocalDate()) + + userMessage.toUiItem() + ) + } + + // 매니저 메세지 flow 구독 + appendManagerMessages(flow) + } + + // 메시지를 로딩 상태로 추가한 뒤, 일정 시간 후 실제 내용을 표시하는 공통 로직 + // + // intent 블록의 리시버가 Syntax이고 reduce가 그 멤버이므로, + // 이 함수도 Syntax 확장 함수로 선언해야 내부에서 reduce를 호출할 수 있다. + private suspend fun Syntax.appendManagerMessages( + flow: Flow + ) { + flow.collect { message -> + reduce { + state.copy( + chatItems = state.chatItems + + dateSeparatorIfNeeded(state.chatItems, message.createdAt.toLocalDate()) + + message.toUiItem(loadStatus = LoadStatus.Loading) + ) + } + + delay(1500.milliseconds) + + reduce { + state.copy( + chatItems = state.chatItems.map { item -> + if (item is ManagerChatUiItem.Message && item.message.id == message.id) + item.copy(loadStatus = LoadStatus.Idle) + else item + } + ) + } + } + } + + // 마지막 메시지의 날짜와 다를 경우에만 DateSeparator를 반환 + private fun dateSeparatorIfNeeded( + chatItems: List, + date: LocalDate + ): List { + + // 현재 채팅 목록에서 마지막 메시지 아이템의 날짜를 가져옴 + // DateSeparator는 제외하고 실제 메시지 데이터만 기준으로 비교 + val lastDate = chatItems + .filterIsInstance() + .lastOrNull() + ?.message?.createdAt?.toLocalDate() + + // 기존 메시지가 없거나 마지막 메시지와 날짜가 다르면 + // 새로운 날짜 구분선을 추가 + return if (lastDate == null || lastDate != date) { + listOf(ManagerChatUiItem.DateSeparator(date)) + } else { + emptyList() + } + } +} \ No newline at end of file