From 8d68f28fd803993f77f1be37808fe1f800943b9f Mon Sep 17 00:00:00 2001 From: princehw03 Date: Sun, 19 Jul 2026 18:52:40 +0900 Subject: [PATCH 01/26] =?UTF-8?q?=E2=9C=A8=20feat(chatting):=20=EB=A7=A4?= =?UTF-8?q?=EB=8B=88=EC=A0=80=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=B0=8F=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=ED=82=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChattingRoute.kt: 매니저 채팅방으로의 이동을 위한 `ManagerChatRoomNayKey` 네비게이션 키 추가 - ManagerChatTopBar.kt: 뒤로가기 버튼, 프로필 이미지, '전화왔어 매니저' 텍스트를 포함한 전용 상단 바 컴포넌트 구현 - ManagerChatRoomScreen.kt: 매니저 채팅방의 기본 레이아웃 구성 및 상단 바 컴포넌트 배치 구체화 --- .../main/java/kr/co/call/api/ChattingRoute.kt | 4 +- .../call/impl/component/ManagerChatTopBar.kt | 69 +++++++++++++++++++ .../call/impl/screen/ManagerChatRoomScreen.kt | 44 ++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/ManagerChatTopBar.kt create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt 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..0c5ca0d 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 @@ -10,4 +10,6 @@ data object ChattingNavKey: NavKey data class ChatRoomNavKey( val roomId: Long, val name: String, -): NavKey \ No newline at end of file +): NavKey + +data object ManagerChatRoomNayKey: NavKey \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/ManagerChatTopBar.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/ManagerChatTopBar.kt new file mode 100644 index 0000000..c41510a --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/ManagerChatTopBar.kt @@ -0,0 +1,69 @@ +package kr.co.call.impl.component + +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.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, + onClick: () -> Unit = {} +) { + Row( + modifier = modifier.background(CallTheme.colors.mainVariant5Chat) + .padding(start = 17.dp, end = 17.dp, top = 6.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_chat_back), + contentDescription = "뒤로가기", + tint = Color.Unspecified, + modifier = Modifier.noRippleClickable( + onClick = onClick + ) + ) + + Spacer(Modifier.width(14.dp)) + + Image( + painter = painterResource(R.drawable.ic_chat_manager), + contentDescription = "매니저", + modifier = Modifier.clip(CircleShape), + 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/screen/ManagerChatRoomScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt new file mode 100644 index 0000000..9a1c0ce --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt @@ -0,0 +1,44 @@ +package kr.co.call.impl.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.impl.component.ManagerChatTopBar + +@Composable +fun ManagerChatRoomScreen( + modifier: Modifier = Modifier, +) { + + ManagerChatRoomScreenContent( + modifier = modifier + ) +} + +@Composable +fun ManagerChatRoomScreenContent( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + ) { + Spacer(Modifier.height(8.dp)) + + ManagerChatTopBar(modifier = Modifier.fillMaxWidth()) + } +} + +@Preview(showBackground = true) +@Composable +private fun ManagerChatRoomScreenPreview() { + CallFromAiTheme { + ManagerChatRoomScreen() + } +} + From 9653a8a6a5694c57f9c310f39670dc6b8c800852 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Sun, 19 Jul 2026 18:53:14 +0900 Subject: [PATCH 02/26] Remove IDE files from Git tracking --- .idea/.gitignore | 3 - .idea/AndroidProjectSystem.xml | 6 -- .idea/claudeCodeTabState.xml | 20 ------- .idea/compiler.xml | 8 --- .idea/deploymentTargetSelector.xml | 18 ------ .idea/deviceManager.xml | 13 ---- .idea/discord.xml | 14 ----- .idea/gradle.xml | 62 -------------------- .idea/inspectionProfiles/Project_Default.xml | 61 ------------------- .idea/kotlinc.xml | 7 --- .idea/markdown.xml | 8 --- .idea/misc.xml | 23 -------- .idea/planningMode.xml | 11 ---- .idea/runConfigurations.xml | 17 ------ .idea/vcs.xml | 6 -- 15 files changed, 277 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/AndroidProjectSystem.xml delete mode 100644 .idea/claudeCodeTabState.xml delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/deploymentTargetSelector.xml delete mode 100644 .idea/deviceManager.xml delete mode 100644 .idea/discord.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/kotlinc.xml delete mode 100644 .idea/markdown.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/planningMode.xml delete mode 100644 .idea/runConfigurations.xml delete mode 100644 .idea/vcs.xml 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 @@ - - - - - - From 2d2a8521e57f7a3b71d32dd0d8c7dc808c4d530f Mon Sep 17 00:00:00 2001 From: princehw03 Date: Sun, 19 Jul 2026 19:51:11 +0900 Subject: [PATCH 03/26] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EB=A7=90?= =?UTF-8?q?=ED=92=8D=EC=84=A0=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20UI=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatGrayBubble, ChatLightPinkBubble, ChatPinkBubble 등 채팅방용 말풍선 컴포넌트 3종 신규 구현 - 디자인 시스템(Color.kt)에 `ChatGradientReverse` 그라데이션 브러시 속성 추가 - 채팅 관련 아이콘 리소스 `ic_chat_telephone.xml` 추가 - ManagerChatTopBar, ChatRoomTopBar의 패키지 위치를 `component.chatroom`으로 이동 및 관련 임포트 경로 수정 - ManagerChatRoomScreen 내 이동된 TopBar 컴포넌트 참조 경로 반영 --- .../kr/co/call/designsystem/theme/Color.kt | 8 +++ .../main/res/drawable/ic_chat_telephone.xml | 9 +++ .../impl/component/chatroom/ChatGrayBubble.kt | 54 ++++++++++++++ .../component/chatroom/ChatLightPinkBubble.kt | 72 +++++++++++++++++++ .../impl/component/chatroom/ChatPinkBubble.kt | 55 ++++++++++++++ .../{ => chatroom}/ChatRoomTopBar.kt | 2 +- .../{ => chatroom}/ManagerChatTopBar.kt | 2 +- .../call/impl/screen/ManagerChatRoomScreen.kt | 2 +- 8 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_chat_telephone.xml create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatGrayBubble.kt create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatLightPinkBubble.kt create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatPinkBubble.kt rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatroom}/ChatRoomTopBar.kt (78%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatroom}/ManagerChatTopBar.kt (98%) 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 a743e51..e63bd6d 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 @@ -52,6 +52,13 @@ val ChatGradient = Brush.horizontalGradient( ) ) +val ChatGradientReverse = Brush.horizontalGradient( + listOf( + Color(0xFFFFA3BD), + Color(0xFFFF789F) + ) +) + // ---------- Design System Colors ---------- @Immutable data class CallColors( @@ -88,6 +95,7 @@ data class CallColors( // Gradient val mainGradient: Brush = MainGradient, val chatGradient: Brush = ChatGradient, + 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/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..64f6270 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatGrayBubble.kt @@ -0,0 +1,54 @@ +package kr.co.call.impl.component.chatroom + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 +) { + Text( + text = text, + modifier = Modifier + .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 + ), + style = CallTheme.typography.bodyMediumMedium.copy( + color = CallTheme.colors.black + ) + ) +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +fun ChatGrayBubblePreview() { + CallFromAiTheme { + ChatGrayBubble( + text = AnnotatedString("안녕하세요! 오늘 날씨가 참 좋네요.") + ) + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatLightPinkBubble.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatLightPinkBubble.kt new file mode 100644 index 0000000..8b49b19 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatLightPinkBubble.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.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.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 + +@Composable +fun ChatLightPinkBubble( + text: String +) { + Row( + 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 + ), + 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 + ) + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +private fun ChatLightPinkBubblePreview() { + CallFromAiTheme { + ChatLightPinkBubble( + text = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요." + ) + } +} 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..719488f --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatPinkBubble.kt @@ -0,0 +1,55 @@ +package kr.co.call.impl.component.chatroom + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 +) { + Text( + text = text, + modifier = Modifier + .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 = "오늘 뭐하고 지냈어? 보고싶다!" + ) + } +} 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/ManagerChatTopBar.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ManagerChatTopBar.kt similarity index 98% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/ManagerChatTopBar.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ManagerChatTopBar.kt index c41510a..319e962 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/ManagerChatTopBar.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ManagerChatTopBar.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component +package kr.co.call.impl.component.chatroom import androidx.compose.foundation.Image import androidx.compose.foundation.background 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 index 9a1c0ce..bff7b43 100644 --- 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 @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.CallFromAiTheme -import kr.co.call.impl.component.ManagerChatTopBar +import kr.co.call.impl.component.chatroom.ManagerChatTopBar @Composable fun ManagerChatRoomScreen( From 8c28b66c8dab1451d695506e29f11ad2cc3f86ae Mon Sep 17 00:00:00 2001 From: princehw03 Date: Sun, 19 Jul 2026 20:38:46 +0900 Subject: [PATCH 04/26] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20UI=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `ManagerChatField.kt`: 메시지 입력 불가 안내를 위한 전용 필드 컴포넌트 추가 - `ManagerPromptSlide.kt`: 드래그 및 애니메이션을 통해 확장 가능한 관리자 문의 프롬프트 슬라이드 컴포넌트 추가 - `ChatLightPinkBubble.kt`: `AnnotatedString`을 지원하는 텍스트 전용 말풍선 컴포넌트 신규 추가 - `ChatLightPinkBubbleWithIcon.kt`: 기존 `ChatLightPinkBubble`을 아이콘 포함 버전으로 명칭 변경 및 `AnnotatedString` 타입 적용 - `ManagerChatTopBar.kt`: 관리자 채팅 컴포넌트 관리를 위해 `chatroom.manager` 하위 패키지로 위치 이동 - `ManagerChatRoomScreen.kt`: 컴포넌트 이동에 따른 패키지 임포트 경로 수정 --- .../chatroom/manager/ChatLightPinkBubble.kt | 66 +++++++++ .../ChatLightPinkBubbleWithIcon.kt} | 11 +- .../chatroom/manager/ManagerChatField.kt | 50 +++++++ .../{ => manager}/ManagerChatTopBar.kt | 2 +- .../chatroom/manager/ManagerPromptSlide.kt | 129 ++++++++++++++++++ .../call/impl/screen/ManagerChatRoomScreen.kt | 2 +- 6 files changed, 253 insertions(+), 7 deletions(-) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubble.kt rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/{ChatLightPinkBubble.kt => manager/ChatLightPinkBubbleWithIcon.kt} (86%) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatField.kt rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/{ => manager}/ManagerChatTopBar.kt (97%) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerPromptSlide.kt 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..d4fe07e --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubble.kt @@ -0,0 +1,66 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 +) { + 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 + ) + ) + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFF000000 +) +@Composable +private fun ChatLightPinkBubblePreview() { + CallFromAiTheme { + ChatLightPinkBubble( + text = buildAnnotatedString { + append("문의를 남겨주시면\n") + withStyle(SpanStyle(color = CallTheme.colors.mainVariant1)) { + append("내용을 확인한 뒤 순차적으로 답변해 드릴게요!") + } + } + ) + } +} diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatLightPinkBubble.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubbleWithIcon.kt similarity index 86% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatLightPinkBubble.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubbleWithIcon.kt index 8b49b19..db528d3 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ChatLightPinkBubble.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubbleWithIcon.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component.chatroom +package kr.co.call.impl.component.chatroom.manager import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row @@ -13,6 +13,7 @@ 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 @@ -20,8 +21,8 @@ import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme @Composable -fun ChatLightPinkBubble( - text: String +fun ChatLightPinkBubbleWithIcon( + text: AnnotatedString ) { Row( modifier = Modifier @@ -65,8 +66,8 @@ fun ChatLightPinkBubble( @Composable private fun ChatLightPinkBubblePreview() { CallFromAiTheme { - ChatLightPinkBubble( - text = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요." + ChatLightPinkBubbleWithIcon( + text = AnnotatedString("대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.") ) } } 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..500707d --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatField.kt @@ -0,0 +1,50 @@ +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.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() + .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/ManagerChatTopBar.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatTopBar.kt similarity index 97% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ManagerChatTopBar.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatTopBar.kt index 319e962..6ea9586 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ManagerChatTopBar.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatTopBar.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.component.chatroom +package kr.co.call.impl.component.chatroom.manager import androidx.compose.foundation.Image import androidx.compose.foundation.background 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..59ffd24 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerPromptSlide.kt @@ -0,0 +1,129 @@ +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 + +@Composable +fun ManagerPromptSlide( + onCallTimeClick: () -> Unit = {}, + onChangePartnerClick: () -> Unit = {}, + onEditRecordClick: () -> Unit = {}, + onInquiryClick: () -> Unit = {} +) { + var isExpanded by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .shadow( + elevation = 8.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 = onCallTimeClick) + PromptItem(emoji = "👤", text = "상대방 정보를 변경하고 싶어요.", onClick = onChangePartnerClick) + + // 슬라이드 업 시 추가 노출 항목 + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically(animationSpec = tween(300)), + exit = shrinkVertically(animationSpec = tween(300)) + ) { + Column { + PromptItem(emoji = "📝", text = "기록한 정보를 수정하고 싶어요.", onClick = onEditRecordClick) + PromptItem(emoji = "💬", text = "상담원에게 문의하기", onClick = onInquiryClick) + } + } + } +} + +@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() + } +} 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 index bff7b43..f4b0acf 100644 --- 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 @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.CallFromAiTheme -import kr.co.call.impl.component.chatroom.ManagerChatTopBar +import kr.co.call.impl.component.chatroom.manager.ManagerChatTopBar @Composable fun ManagerChatRoomScreen( From 214a76e9b8a806649055836e53eb77f01361360c Mon Sep 17 00:00:00 2001 From: princehw03 Date: Sun, 19 Jul 2026 20:54:42 +0900 Subject: [PATCH 05/26] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EC=8B=9D=EB=B3=84=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/{ => chatlist}/BackgroundRow.kt | 2 +- .../component/{ => chatlist}/ChatListItem.kt | 4 +- .../impl/component/{ => chatlist}/FrontRow.kt | 2 +- .../component/{ => chatlist}/LoadingColumn.kt | 2 +- .../component/{ => chatlist}/SwipeState.kt | 2 +- .../component/{ => chatlist}/SwipeableBox.kt | 2 +- .../{ => chatlist}/UnReadMessageCountBox.kt | 5 +-- .../impl/component/chatroom/DateSeparator.kt | 38 +++++++++++++++++++ .../kr/co/call/impl/screen/ChatListScreen.kt | 6 +-- 9 files changed, 49 insertions(+), 14 deletions(-) rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatlist}/BackgroundRow.kt (98%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatlist}/ChatListItem.kt (95%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatlist}/FrontRow.kt (99%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatlist}/LoadingColumn.kt (96%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatlist}/SwipeState.kt (53%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatlist}/SwipeableBox.kt (98%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/component/{ => chatlist}/UnReadMessageCountBox.kt (87%) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/DateSeparator.kt 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/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/screen/ChatListScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatListScreen.kt index a1bed94..1ad4920 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,9 +19,9 @@ 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.component.chatlist.ChatListItem +import kr.co.call.impl.component.chatlist.FrontRow +import kr.co.call.impl.component.chatlist.LoadingColumn import kr.co.call.impl.viewmodel.ChatListIntent import kr.co.call.impl.viewmodel.ChatListSideEffect import kr.co.call.impl.viewmodel.ChatListState From 60d58c6a202507c7335eac822157e6a24c405b13 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Sun, 19 Jul 2026 21:27:46 +0900 Subject: [PATCH 06/26] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EB=B0=8F=20=EC=9C=A0=EC=8A=A4=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerFirstMessage: 초기 메시지 정보를 담는 데이터 클래스 및 유형(NORMAL, RELATIONSHIP) 정의 - FirstManagerChatUseCase: 채팅 시작 시 사용자에게 보여줄 초기 안내 메시지 리스트를 반환하는 유스케이스 구현 - 서비스 소개, 관계 형성 가이드 등 미리 정의된 초기 대화 시나리오 로직 추가 --- .../domain/model/chatting/ManagerMessage.kt | 15 ++++++ .../chatting/FirstManagerChatUseCase.kt | 48 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/usecase/chatting/FirstManagerChatUseCase.kt diff --git a/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt new file mode 100644 index 0000000..e8243e4 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt @@ -0,0 +1,15 @@ +package kr.co.call.domain.model.chatting + +import java.time.LocalDateTime + +data class ManagerFirstMessage( + val id: String, + val content: String, + val type: ManagerFirstMessageType, + val createdAt: LocalDateTime, +) + +enum class ManagerFirstMessageType { + NORMAL, + RELATIONSHIP // 대화를 많이 나눌수록, 더 자연스러운 관계가 생성돼요. +} \ No newline at end of file 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..39248be --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/FirstManagerChatUseCase.kt @@ -0,0 +1,48 @@ +package kr.co.call.domain.usecase.chatting + +import kr.co.call.domain.model.chatting.ManagerFirstMessage +import kr.co.call.domain.model.chatting.ManagerFirstMessageType +import java.time.LocalDateTime +import javax.inject.Inject + +/** + * 사용자가 매니저 채팅을 처음 시작할 때 매니저(AI)가 보내는 초기 메시지 세트를 제공하는 유스케이스입니다. + * + * 환영 인사, 서비스 소개, 관계 형성 설명 및 일반적인 안내 사항을 포함하는 + * 미리 정의된 메시지 시퀀스를 반환합니다. + * + * @return 매니저의 오프닝 대화를 나타내는 [ManagerFirstMessage] 리스트. + */ +class FirstManagerChatUseCase @Inject constructor() { + suspend operator fun invoke(): List { + val now = LocalDateTime.now() + + return listOf( + ManagerFirstMessage( + id = "welcome", + content = "수현님, 반가워요! 👋🏻", + type = ManagerFirstMessageType.NORMAL, + createdAt = now + ), + ManagerFirstMessage( + id = "intro", + content = "전화왔어는 AI가 먼저 연락하고,\n사용자를 기억하며 관계를 만들어가는 서비스예요.", + type = ManagerFirstMessageType.NORMAL, + createdAt = now + ), + ManagerFirstMessage( + id = "relationship", + content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", + type = ManagerFirstMessageType.RELATIONSHIP, + createdAt = now + ), + ManagerFirstMessage( + id = "guide", + content = "필요한 안내는 언제든 이 채팅방에서 확인해주세요!", + type = ManagerFirstMessageType.NORMAL, + createdAt = now + ) + ) + } + +} \ No newline at end of file From 36739e626d2774118eb9661e0b02d75034ff2296 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Sun, 19 Jul 2026 22:48:23 +0900 Subject: [PATCH 07/26] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20UI=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=20=EC=9A=94=EC=86=8C=20=EA=B3=A0=EB=8F=84?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `LoadingIndicator`: 세 개의 점이 순차적으로 튀어오르는 애니메이션 컴포넌트 신규 생성 - `ChatGrayBubble`: 로딩 상태(`isLoading`) 추가 및 로딩 중일 때 `LoadingIndicator`를 표시하도록 수정 - `ChatGrayBubble`, `ChatPinkBubble`, `ChatLightPinkBubble`: 모든 채팅 말풍선에 메시지 전송 시간(`time`) 표시 레이아웃 적용 - `FirstManagerChatUseCase`: 초기 메시지 반환 타입을 `List`에서 `Flow`로 변경하고, 메시지 간 1.5초의 지연 시간을 두어 순차적으로 방출하도록 수정 --- .../chatting/FirstManagerChatUseCase.kt | 25 ++-- .../impl/component/chatroom/ChatGrayBubble.kt | 80 ++++++++---- .../impl/component/chatroom/ChatPinkBubble.kt | 61 +++++---- .../component/chatroom/LoadingIndicator.kt | 116 ++++++++++++++++++ .../chatroom/manager/ChatLightPinkBubble.kt | 57 +++++---- .../manager/ChatLightPinkBubbleWithIcon.kt | 69 ++++++----- 6 files changed, 305 insertions(+), 103 deletions(-) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/LoadingIndicator.kt 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 index 39248be..6242c35 100644 --- 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 @@ -1,23 +1,24 @@ 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.ManagerFirstMessage import kr.co.call.domain.model.chatting.ManagerFirstMessageType import java.time.LocalDateTime import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds /** - * 사용자가 매니저 채팅을 처음 시작할 때 매니저(AI)가 보내는 초기 메시지 세트를 제공하는 유스케이스입니다. - * - * 환영 인사, 서비스 소개, 관계 형성 설명 및 일반적인 안내 사항을 포함하는 - * 미리 정의된 메시지 시퀀스를 반환합니다. - * - * @return 매니저의 오프닝 대화를 나타내는 [ManagerFirstMessage] 리스트. + * 사용자가 매니저 채팅을 처음 시작할 때 매니저(AI)가 보내는 초기 메시지를 + * 순차적으로 방출하는 유스케이스입니다. */ class FirstManagerChatUseCase @Inject constructor() { - suspend operator fun invoke(): List { + + operator fun invoke(): Flow = flow { val now = LocalDateTime.now() - return listOf( + val messages = listOf( ManagerFirstMessage( id = "welcome", content = "수현님, 반가워요! 👋🏻", @@ -43,6 +44,12 @@ class FirstManagerChatUseCase @Inject constructor() { createdAt = now ) ) - } + messages.forEachIndexed { index, message -> + if (index != 0) { + delay(1500.milliseconds) + } + emit(message) + } + } } \ No newline at end of file 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 index 64f6270..8a8d300 100644 --- 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 @@ -1,10 +1,14 @@ 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.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 @@ -14,30 +18,48 @@ import kr.co.call.designsystem.theme.CallTheme @Composable fun ChatGrayBubble( - text: AnnotatedString + text: AnnotatedString, + time: String, + isLoading: Boolean = false, ) { - Text( - text = text, - modifier = Modifier - .background( - color = CallTheme.colors.gray100, - shape = RoundedCornerShape( - topStart = 0.dp, - topEnd = 20.dp, - bottomStart = 20.dp, - bottomEnd = 20.dp + // Modifier는 bubbleModifier 변수로 분리해 두 분기가 공유하도록 함 + val bubbleModifier = Modifier + .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 ) ) - .padding( - start = 23.dp, - end = 21.dp, - top = 9.dp, - bottom = 9.dp - ), - 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( @@ -48,7 +70,23 @@ fun ChatGrayBubble( fun ChatGrayBubblePreview() { CallFromAiTheme { ChatGrayBubble( - text = AnnotatedString("안녕하세요! 오늘 날씨가 참 좋네요.") + 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 index 719488f..bcf3a95 100644 --- 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 @@ -1,10 +1,14 @@ 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.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 @@ -14,31 +18,41 @@ import kr.co.call.designsystem.theme.CallTheme @Composable fun ChatPinkBubble( - text: String + text: String, + time: String ) { - Text( - text = text, - modifier = Modifier - .background( - brush = CallTheme.colors.chatGradientReverse, - shape = RoundedCornerShape( - topStart = 20.dp, - topEnd = 20.dp, - bottomStart = 20.dp, - bottomEnd = 0.dp - ) + Row(verticalAlignment = Alignment.Bottom) { + Text( + text = time, + style = CallTheme.typography.caption.copy( + color = CallTheme.colors.gray400 ) - .padding( - start = 23.dp, - end = 21.dp, - top = 9.dp, - bottom = 9.dp + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = text, + modifier = Modifier + .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 ), - style = CallTheme.typography.bodyMediumMedium.copy( - color = CallTheme.colors.white - ), - textAlign = TextAlign.End - ) + textAlign = TextAlign.End + ) + } } @Preview( @@ -49,7 +63,8 @@ fun ChatPinkBubble( fun ChatPinkBubblePreview() { CallFromAiTheme { ChatPinkBubble( - text = "오늘 뭐하고 지냈어? 보고싶다!" + text = "오늘 뭐하고 지냈어? 보고싶다!", + time = "12:01" ) } } 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..56f8d90 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/LoadingIndicator.kt @@ -0,0 +1,116 @@ +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, +) { + val transition = rememberInfiniteTransition(label = "typing") + + val progress by transition.animateFloat( + initialValue = 0f, + targetValue = 0f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 900 + 0f at 0 using FastOutSlowInEasing + 1f at 300 using FastOutSlowInEasing // 300ms에 최고점 + 0f at 600 using FastOutSlowInEasing // 600ms에 원위치 + // 600~900ms는 대기(idle) + }, + initialStartOffset = StartOffset(delayMillis), + ), + label = "dotOffset", + ) + + Box( + modifier = Modifier + .size(size) + .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/manager/ChatLightPinkBubble.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubble.kt index d4fe07e..81cb059 100644 --- 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 @@ -2,10 +2,14 @@ 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 @@ -18,30 +22,40 @@ import kr.co.call.designsystem.theme.CallTheme @Composable fun ChatLightPinkBubble( - text: AnnotatedString + text: AnnotatedString, + time: String ) { - Box( - modifier = Modifier - .background( - color = CallTheme.colors.mainVariant5Chat, - shape = RoundedCornerShape( - topStart = 0.dp, - topEnd = 20.dp, - bottomStart = 20.dp, - bottomEnd = 20.dp + 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 ) ) - .padding( - start = 23.dp, - end = 21.dp, - top = 9.dp, - bottom = 9.dp - ) - ) { + } + Spacer(modifier = Modifier.width(6.dp)) Text( - text = text, - style = CallTheme.typography.bodyMediumMedium.copy( - color = CallTheme.colors.black + text = time, + style = CallTheme.typography.caption.copy( + color = CallTheme.colors.gray400 ) ) } @@ -60,7 +74,8 @@ private fun ChatLightPinkBubblePreview() { 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 index db528d3..550c663 100644 --- 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 @@ -22,38 +22,48 @@ import kr.co.call.designsystem.theme.CallTheme @Composable fun ChatLightPinkBubbleWithIcon( - text: AnnotatedString + text: AnnotatedString, + time: String ) { - Row( - modifier = Modifier - .background( - color = CallTheme.colors.mainVariant5Chat, - shape = RoundedCornerShape( - topStart = 0.dp, - topEnd = 20.dp, - bottomStart = 20.dp, - bottomEnd = 20.dp + Row(verticalAlignment = Alignment.Bottom) { + Row( + 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 + ), + 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 ) - .padding( - start = 23.dp, - end = 21.dp, - top = 9.dp, - bottom = 9.dp - ), - 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)) + 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 = text, - style = CallTheme.typography.bodyMediumMedium.copy( - color = CallTheme.colors.black + text = time, + style = CallTheme.typography.caption.copy( + color = CallTheme.colors.gray400 ) ) } @@ -67,7 +77,8 @@ fun ChatLightPinkBubbleWithIcon( private fun ChatLightPinkBubblePreview() { CallFromAiTheme { ChatLightPinkBubbleWithIcon( - text = AnnotatedString("대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.") + text = AnnotatedString("대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요."), + time = "12:01" ) } } From 1c4475965464cf28c43b68be6ee73c7baf710698 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 00:40:30 +0900 Subject: [PATCH 08/26] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20=EC=B4=88=EA=B8=B0=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EC=A7=80=20=EB=A1=9C=EB=93=9C=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/data/di/UseCaseModule.kt | 9 ++ .../domain/model/chatting/ManagerMessage.kt | 11 +- .../chatting/FirstManagerChatUseCase.kt | 37 +++--- .../call/impl/component/chatroom/ai/gitkeep | 0 .../manager/ChatLightPinkBubbleWithIcon.kt | 90 ++++++++++----- .../chatroom/manager/ManagerChatLazyColumn.kt | 109 ++++++++++++++++++ .../kr/co/call/impl/mapper/UiModelMapper.kt | 14 +++ .../kr/co/call/impl/screen/ChatListScreen.kt | 40 ++++--- .../call/impl/screen/ManagerChatRoomScreen.kt | 51 +++++++- .../impl/viewmodel/ManagerChatRoomUiState.kt | 6 + .../viewmodel/ManagerChatRoomViewModel.kt | 61 ++++++++++ .../call/impl/viewmodel/ManagerChatUiItem.kt | 10 ++ 12 files changed, 368 insertions(+), 70 deletions(-) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/ai/gitkeep create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatLazyColumn.kt create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomUiState.kt create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomViewModel.kt create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatUiItem.kt 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..a733bd6 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,19 @@ 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 javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object UseCaseModule { + + @Provides + @Singleton + fun provideFirstManagerChatUseCase(): FirstManagerChatUseCase { + return FirstManagerChatUseCase() + } } diff --git a/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt index e8243e4..4f2f63c 100644 --- a/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt +++ b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt @@ -2,12 +2,17 @@ package kr.co.call.domain.model.chatting import java.time.LocalDateTime +sealed interface ManagerChatItem { + val id: String + val createdAt: LocalDateTime +} + data class ManagerFirstMessage( - val id: String, + override val id: String, val content: String, val type: ManagerFirstMessageType, - val createdAt: LocalDateTime, -) + override val createdAt: LocalDateTime, +): ManagerChatItem enum class ManagerFirstMessageType { NORMAL, 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 index 6242c35..9b59607 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -10,33 +11,50 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds /** - * 사용자가 매니저 채팅을 처음 시작할 때 매니저(AI)가 보내는 초기 메시지를 + * 매니저 채팅방에 처음 진입했을 때 표시할 초기 메시지 목록을 * 순차적으로 방출하는 유스케이스입니다. + * + * 메시지의 표시 간격이나 로딩 연출은 담당하지 않으며, + * 초기에 노출해야 할 메시지 데이터만 제공합니다. */ class FirstManagerChatUseCase @Inject constructor() { - operator fun invoke(): Flow = flow { + /** + * 초기 매니저 메시지를 [Flow] 형태로 순차 방출합니다. + * + * @return 초기 매니저 채팅 메시지 스트림 + */ + operator fun invoke(): Flow = flow { val now = LocalDateTime.now() - val messages = listOf( + emit( ManagerFirstMessage( id = "welcome", content = "수현님, 반가워요! 👋🏻", type = ManagerFirstMessageType.NORMAL, createdAt = now - ), + ) + ) + + emit( ManagerFirstMessage( id = "intro", content = "전화왔어는 AI가 먼저 연락하고,\n사용자를 기억하며 관계를 만들어가는 서비스예요.", type = ManagerFirstMessageType.NORMAL, createdAt = now - ), + ) + ) + + emit( ManagerFirstMessage( id = "relationship", content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", type = ManagerFirstMessageType.RELATIONSHIP, createdAt = now - ), + ) + ) + + emit( ManagerFirstMessage( id = "guide", content = "필요한 안내는 언제든 이 채팅방에서 확인해주세요!", @@ -44,12 +62,5 @@ class FirstManagerChatUseCase @Inject constructor() { createdAt = now ) ) - - messages.forEachIndexed { index, message -> - if (index != 0) { - delay(1500.milliseconds) - } - emit(message) - } } } \ No newline at end of file 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/ChatLightPinkBubbleWithIcon.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ChatLightPinkBubbleWithIcon.kt index 550c663..02ecf54 100644 --- 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 @@ -19,47 +19,60 @@ 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 + 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) { - Row( - modifier = Modifier - .background( - color = CallTheme.colors.mainVariant5Chat, - shape = RoundedCornerShape( - topStart = 0.dp, - topEnd = 20.dp, - bottomStart = 20.dp, - bottomEnd = 20.dp - ) + 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 ) - .padding( - start = 23.dp, - end = 21.dp, - top = 9.dp, - bottom = 9.dp - ), - 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(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( @@ -82,3 +95,18 @@ private fun ChatLightPinkBubblePreview() { ) } } + +@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/ManagerChatLazyColumn.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatLazyColumn.kt new file mode 100644 index 0000000..534690c --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatLazyColumn.kt @@ -0,0 +1,109 @@ +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.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.ManagerFirstMessage +import kr.co.call.domain.model.chatting.ManagerFirstMessageType +import kr.co.call.domain.util.LoadStatus +import kr.co.call.impl.component.chatroom.ChatGrayBubble +import kr.co.call.impl.viewmodel.ManagerChatUiItem + +@Composable +fun ManagerChatLazyColumn( + chatItems: List, + modifier: Modifier = Modifier, + listState: LazyListState = rememberLazyListState(), +) { + LazyColumn( + state = listState, + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(11.dp), + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 13.dp + ) + ) { + items( + items = chatItems, + key = { it.message.id } + ) { item -> + when(val message = item.message) { + + is ManagerFirstMessage -> { + when (message.type) { + + ManagerFirstMessageType.NORMAL -> { + ChatGrayBubble( + text = AnnotatedString(message.content), + time = item.time, + isLoading = item.loadStatus == LoadStatus.Loading + ) + } + + ManagerFirstMessageType.RELATIONSHIP -> { + ChatLightPinkBubbleWithIcon( + text = AnnotatedString(message.content), + time = item.time, + isLoading = item.loadStatus == LoadStatus.Loading + ) + } + } + } + + } + } + + } +} + +@Preview(showBackground = true) +@Composable +private fun ManagerChatLazyColumnPreview() { + val sampleChatItems = listOf( + ManagerChatUiItem( + message = ManagerFirstMessage( + id = "1", + content = "안녕하세요! 무엇을 도와드릴까요?", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + time = "오후 2:00" + ), + ManagerChatUiItem( + message = ManagerFirstMessage( + id = "2", + content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", + type = ManagerFirstMessageType.RELATIONSHIP, + createdAt = LocalDateTime.now() + ), + time = "오후 2:01" + ), + ManagerChatUiItem( + 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/mapper/UiModelMapper.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/mapper/UiModelMapper.kt index cbb9604..c3524a0 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.viewmodel.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 = + ManagerChatUiItem( + 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/screen/ChatListScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatListScreen.kt index 1ad4920..48c1c46 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 @@ -114,8 +114,9 @@ fun ChatListScreenContent( isManager = true, chatSummary = ChatSummary( name = "전화왔어 매니저", - content = "수현님, 반가워요! 👋🏻 오늘은 어떤 이야기를" - ) + content = "수현님, 반가워요! 👋🏻 오늘은 어떤 이야기를", + ), + onClick = { } ) } } @@ -126,33 +127,34 @@ 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 + ), + onChatRoomClick = { _, _ -> }, + onIntent = {} ) } } 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 index f4b0acf..3c040ea 100644 --- 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 @@ -1,44 +1,87 @@ 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.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.statusBarsPadding 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.hilt.navigation.compose.hiltViewModel +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.ManagerChatLazyColumn import kr.co.call.impl.component.chatroom.manager.ManagerChatTopBar +import kr.co.call.impl.viewmodel.ManagerChatRoomUiState +import kr.co.call.impl.viewmodel.ManagerChatRoomViewModel +import kr.co.call.impl.viewmodel.ManagerChatUiItem +import org.orbitmvi.orbit.compose.collectAsState @Composable fun ManagerChatRoomScreen( modifier: Modifier = Modifier, + viewModel: ManagerChatRoomViewModel = hiltViewModel() ) { - + // 상태 구독 + val state = viewModel.collectAsState().value + ManagerChatRoomScreenContent( - modifier = modifier + modifier = modifier, + state = state // 상태 호이스팅 ) } @Composable fun ManagerChatRoomScreenContent( modifier: Modifier = Modifier, + state: ManagerChatRoomUiState, ) { Column( modifier = modifier + .fillMaxSize() + .background(CallTheme.colors.mainVariant5Chat) + .statusBarsPadding() ) { Spacer(Modifier.height(8.dp)) ManagerChatTopBar(modifier = Modifier.fillMaxWidth()) + + ManagerChatLazyColumn( + modifier = Modifier.fillMaxWidth() + .weight(1f) + .background(CallTheme.colors.background), + chatItems = state.chatItems + ) } } @Preview(showBackground = true) @Composable -private fun ManagerChatRoomScreenPreview() { +private fun ManagerChatRoomScreenContentPreview() { CallFromAiTheme { - ManagerChatRoomScreen() + ManagerChatRoomScreenContent( + state = ManagerChatRoomUiState( + chatItems = listOf( + ManagerChatUiItem( + message = ManagerFirstMessage( + id = "1", + content = "안녕하세요! 무엇을 도와드릴까요?", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + time = "오전 10:00" + ) + ) + ) + ) } } + diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomUiState.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomUiState.kt new file mode 100644 index 0000000..5312621 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomUiState.kt @@ -0,0 +1,6 @@ +package kr.co.call.impl.viewmodel + + +data class ManagerChatRoomUiState( + val chatItems: List = emptyList(), +) 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..8ae3b94 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomViewModel.kt @@ -0,0 +1,61 @@ +package kr.co.call.impl.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kr.co.call.domain.usecase.chatting.FirstManagerChatUseCase +import kr.co.call.domain.util.LoadStatus +import kr.co.call.impl.mapper.UiModelMapper.toUiItem +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +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 +): ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = ManagerChatRoomUiState() + ) + + init { + collectFirstMessages() + } + + // 초기 매니저 메시지를 순차적으로 수집하여 채팅 목록에 추가 + // 각 메시지는 먼저 로딩 상태로 노출한 뒤, 일정 시간이 지나면 로딩을 해제하여 실제 메시지 내용을 표시 + private fun collectFirstMessages() = intent { + firstManagerChatUseCase().collect { message -> + + // 로딩 상태의 메시지를 채팅 목록에 추가 + reduce { + state.copy( + chatItems = state.chatItems + message.toUiItem( + loadStatus = LoadStatus.Loading + ) + ) + } + + // 매니저가 입력 중인 것처럼 보이도록 일정 시간 대기 + delay(1500.milliseconds) + + // 동일한 메시지의 로딩 상태를 해제하여 실제 내용을 표시 + reduce { + state.copy( + chatItems = state.chatItems.map { item -> + // 현재 처리 중인 메시지라면 로딩 상태를 해제 + if (item.message.id == message.id) { + item.copy(loadStatus = LoadStatus.Idle) + } else { + item + } + } + ) + } + } + } + +} \ No newline at end of file diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatUiItem.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatUiItem.kt new file mode 100644 index 0000000..39609a6 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatUiItem.kt @@ -0,0 +1,10 @@ +package kr.co.call.impl.viewmodel + +import kr.co.call.domain.model.chatting.ManagerChatItem +import kr.co.call.domain.util.LoadStatus + +data class ManagerChatUiItem( + val message: ManagerChatItem, + val loadStatus: LoadStatus = LoadStatus.Idle, + val time: String = "", +) \ No newline at end of file From 3b12a9538192fdd1e8ea16dce657c5e9c98d5c46 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 00:47:18 +0900 Subject: [PATCH 09/26] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99=20=EC=8B=9C=20roomId=EB=A7=8C=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/kr/co/call/callfromai/AppScreen.kt | 4 ++-- .../api/src/main/java/kr/co/call/api/ChattingRoute.kt | 1 - .../kr/co/call/impl/entry/ChattingEntryBuilder.kt | 3 +-- .../java/kr/co/call/impl/screen/ChatListScreen.kt | 11 +++++------ .../java/kr/co/call/impl/screen/ChatRoomScreen.kt | 1 - .../java/kr/co/call/impl/viewmodel/ChatListIntent.kt | 2 +- .../kr/co/call/impl/viewmodel/ChatListSideEffect.kt | 2 +- .../kr/co/call/impl/viewmodel/ChatListViewModel.kt | 6 +++--- 8 files changed, 13 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 913f726..dd3d57d 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -104,8 +104,8 @@ 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)) }, onBack = { appNavigator.popBackStack() 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 0c5ca0d..1521287 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,7 +9,6 @@ data object ChattingNavKey: NavKey @Serializable data class ChatRoomNavKey( val roomId: Long, - val name: String, ): NavKey data object ManagerChatRoomNayKey: NavKey \ 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..8612e8f 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 @@ -8,7 +8,7 @@ import kr.co.call.impl.screen.ChatListScreen import kr.co.call.impl.screen.ChatRoomScreen fun EntryProviderScope.chattingEntry( - navigateToChatRoom: (Long, String) -> Unit, + navigateToChatRoom: (Long) -> Unit, onBack: () -> Unit = {} ) { entry { @@ -20,7 +20,6 @@ fun EntryProviderScope.chattingEntry( entry { key -> ChatRoomScreen( roomId = key.roomId, - name = key.name, onBack = onBack ) } 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 48c1c46..3a59b11 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 @@ -32,7 +32,7 @@ import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun ChatListScreen( modifier: Modifier = Modifier, - onChatRoomClick: (Long, String) -> Unit, + onChatRoomClick: (Long) -> Unit, viewModel: ChatListViewModel = hiltViewModel() ) { // 상태 구독 @@ -41,7 +41,7 @@ fun ChatListScreen( // 사이드이펙트 수신 viewModel.collectSideEffect { sideEffect -> when (sideEffect) { - is ChatListSideEffect.NavigateToChatRoom -> onChatRoomClick(sideEffect.roomId, sideEffect.name) + is ChatListSideEffect.NavigateToChatRoom -> onChatRoomClick(sideEffect.roomId) } } @@ -56,7 +56,7 @@ fun ChatListScreen( @Composable fun ChatListScreenContent( state: ChatListState, - onChatRoomClick: (Long, String) -> Unit, + onChatRoomClick: (Long) -> 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 ) ) } @@ -153,7 +152,7 @@ private fun ChatListScreenContentPreview() { ), status = LoadStatus.Idle ), - onChatRoomClick = { _, _ -> }, + onChatRoomClick = { _ -> }, 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/viewmodel/ChatListIntent.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListIntent.kt index 3235ce6..026e2b5 100644 --- 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 @@ -1,5 +1,5 @@ package kr.co.call.impl.viewmodel sealed interface ChatListIntent { - data class ClickChatRoom(val roomId: Long, val name: String) : ChatListIntent + data class ClickChatRoom(val roomId: Long) : 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 index 0d26ff7..c9b8cc4 100644 --- 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 @@ -1,5 +1,5 @@ package kr.co.call.impl.viewmodel sealed interface ChatListSideEffect { - data class NavigateToChatRoom(val roomId: Long, val name: String): ChatListSideEffect + data class NavigateToChatRoom(val roomId: Long): 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..c256da3 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 @@ -47,12 +47,12 @@ class ChatListViewModel @Inject constructor( fun handleIntent(intent: ChatListIntent) { when (intent) { - is ChatListIntent.ClickChatRoom -> emitNavigateToChatRoom(intent.roomId, intent.name) + is ChatListIntent.ClickChatRoom -> emitNavigateToChatRoom(intent.roomId) } } - private fun emitNavigateToChatRoom(roomId: Long, name: String) = intent { - postSideEffect(ChatListSideEffect.NavigateToChatRoom(roomId, name)) + private fun emitNavigateToChatRoom(roomId: Long) = intent { + postSideEffect(ChatListSideEffect.NavigateToChatRoom(roomId)) } From cb465c034e37bb1328a0ea80eeb3a4b5f0421684 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 01:26:08 +0900 Subject: [PATCH 10/26] =?UTF-8?q?=E2=9C=A8=20feat(chatting):=20=EB=A7=A4?= =?UTF-8?q?=EB=8B=88=EC=A0=80=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20UI=20=EC=84=B8=EB=B6=80=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `ChatListViewModel`, `Intent`, `SideEffect`에 매니저 채팅방 클릭 관련 로직 추가 - `ChattingEntryBuilder` 및 `AppScreen`에 `ManagerChatRoomNayKey` 목적지 등록 및 네비게이션 연동 - `ChatListScreen`에서 매니저 채팅 아이템 클릭 시 화면 전환 처리 추가 - `ManagerChatRoomScreen` 및 `ManagerChatTopBar`에 뒤로가기(`onBack`) 콜백 기능 반영 - `ChatGrayBubble` 말풍선의 최대 너비를 280.dp로 제한하도록 수정 - `ManagerChatTopBar` 프로필 이미지 크기 고정(40.dp) 및 하단 패딩 조정 --- app/src/main/java/kr/co/call/callfromai/AppScreen.kt | 6 +++++- .../call/impl/component/chatroom/ChatGrayBubble.kt | 2 ++ .../component/chatroom/manager/ManagerChatTopBar.kt | 10 ++++++---- .../kr/co/call/impl/entry/ChattingEntryBuilder.kt | 12 +++++++++++- .../java/kr/co/call/impl/screen/ChatListScreen.kt | 7 ++++++- .../kr/co/call/impl/screen/ManagerChatRoomScreen.kt | 11 +++++++++-- .../java/kr/co/call/impl/viewmodel/ChatListIntent.kt | 2 ++ .../kr/co/call/impl/viewmodel/ChatListSideEffect.kt | 3 ++- .../kr/co/call/impl/viewmodel/ChatListViewModel.kt | 6 ++++-- 9 files changed, 47 insertions(+), 12 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 dd3d57d..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 } @@ -107,6 +108,9 @@ fun AppScreen(modifier: Modifier = Modifier) { navigateToChatRoom = { roomId -> appNavigator.navigate(ChatRoomNavKey(roomId = roomId)) }, + navigateToManagerChatRoom = { + appNavigator.navigate(ManagerChatRoomNayKey) + }, onBack = { appNavigator.popBackStack() } 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 index 8a8d300..198c05b 100644 --- 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 @@ -5,6 +5,7 @@ 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 @@ -24,6 +25,7 @@ fun ChatGrayBubble( ) { // Modifier는 bubbleModifier 변수로 분리해 두 분기가 공유하도록 함 val bubbleModifier = Modifier + .widthIn(max = 280.dp) .background( color = CallTheme.colors.gray100, shape = RoundedCornerShape( 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 index 6ea9586..49f4522 100644 --- 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 @@ -5,6 +5,7 @@ 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 @@ -26,11 +27,11 @@ import kr.co.call.designsystem.theme.CallTheme @Composable fun ManagerChatTopBar( modifier: Modifier = Modifier, - onClick: () -> Unit = {} + onBack: () -> Unit = {} ) { Row( modifier = modifier.background(CallTheme.colors.mainVariant5Chat) - .padding(start = 17.dp, end = 17.dp, top = 6.dp, bottom = 12.dp), + .padding(start = 17.dp, end = 17.dp, top = 6.dp, bottom = 20.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -38,7 +39,7 @@ fun ManagerChatTopBar( contentDescription = "뒤로가기", tint = Color.Unspecified, modifier = Modifier.noRippleClickable( - onClick = onClick + onClick = onBack ) ) @@ -47,7 +48,8 @@ fun ManagerChatTopBar( Image( painter = painterResource(R.drawable.ic_chat_manager), contentDescription = "매니저", - modifier = Modifier.clip(CircleShape), + modifier = Modifier.clip(CircleShape) + .size(40.dp), contentScale = ContentScale.Crop ) 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 8612e8f..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,16 +4,20 @@ 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) -> Unit, + navigateToManagerChatRoom: () -> Unit, onBack: () -> Unit = {} ) { entry { ChatListScreen( - onChatRoomClick = navigateToChatRoom + onChatRoomClick = navigateToChatRoom, + onManagerChatRoomClick = navigateToManagerChatRoom ) } @@ -23,4 +27,10 @@ fun EntryProviderScope.chattingEntry( onBack = onBack ) } + + entry { + ManagerChatRoomScreen( + onBack = onBack + ) + } } 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 3a59b11..2f5c6cf 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 @@ -33,6 +33,7 @@ import org.orbitmvi.orbit.compose.collectSideEffect fun ChatListScreen( modifier: Modifier = Modifier, onChatRoomClick: (Long) -> Unit, + onManagerChatRoomClick: () -> Unit, viewModel: ChatListViewModel = hiltViewModel() ) { // 상태 구독 @@ -42,12 +43,14 @@ fun ChatListScreen( viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is ChatListSideEffect.NavigateToChatRoom -> onChatRoomClick(sideEffect.roomId) + ChatListSideEffect.NavigateToManagerChatRoom -> onManagerChatRoomClick() } } ChatListScreenContent( state = state, onChatRoomClick = onChatRoomClick, + onManagerChatRoomClick = onManagerChatRoomClick, onIntent = viewModel::handleIntent, modifier = modifier ) @@ -57,6 +60,7 @@ fun ChatListScreen( fun ChatListScreenContent( state: ChatListState, onChatRoomClick: (Long) -> Unit, + onManagerChatRoomClick: () -> Unit, onIntent: (ChatListIntent) -> Unit, modifier: Modifier = Modifier, ) { @@ -115,7 +119,7 @@ fun ChatListScreenContent( name = "전화왔어 매니저", content = "수현님, 반가워요! 👋🏻 오늘은 어떤 이야기를", ), - onClick = { } + onClick = onManagerChatRoomClick ) } } @@ -153,6 +157,7 @@ private fun ChatListScreenContentPreview() { status = LoadStatus.Idle ), onChatRoomClick = { _ -> }, + onManagerChatRoomClick = {}, onIntent = {} ) } 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 index 3c040ea..d75842e 100644 --- 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 @@ -27,6 +27,7 @@ import org.orbitmvi.orbit.compose.collectAsState @Composable fun ManagerChatRoomScreen( modifier: Modifier = Modifier, + onBack: () -> Unit, viewModel: ManagerChatRoomViewModel = hiltViewModel() ) { // 상태 구독 @@ -34,6 +35,7 @@ fun ManagerChatRoomScreen( ManagerChatRoomScreenContent( modifier = modifier, + onBack = onBack, state = state // 상태 호이스팅 ) } @@ -41,6 +43,7 @@ fun ManagerChatRoomScreen( @Composable fun ManagerChatRoomScreenContent( modifier: Modifier = Modifier, + onBack: () -> Unit, state: ManagerChatRoomUiState, ) { Column( @@ -51,7 +54,10 @@ fun ManagerChatRoomScreenContent( ) { Spacer(Modifier.height(8.dp)) - ManagerChatTopBar(modifier = Modifier.fillMaxWidth()) + ManagerChatTopBar( + modifier = Modifier.fillMaxWidth(), + onBack = onBack + ) ManagerChatLazyColumn( modifier = Modifier.fillMaxWidth() @@ -79,7 +85,8 @@ private fun ManagerChatRoomScreenContentPreview() { time = "오전 10:00" ) ) - ) + ), + onBack = {} ) } } 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 index 026e2b5..44b19d6 100644 --- 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 @@ -2,4 +2,6 @@ package kr.co.call.impl.viewmodel 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/viewmodel/ChatListSideEffect.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListSideEffect.kt index c9b8cc4..1873fbf 100644 --- 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 @@ -2,4 +2,5 @@ package kr.co.call.impl.viewmodel sealed interface ChatListSideEffect { data class NavigateToChatRoom(val roomId: Long): ChatListSideEffect -} \ No newline at end of file + data object NavigateToManagerChatRoom: ChatListSideEffect +} 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 c256da3..1acc9e7 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 @@ -48,6 +48,7 @@ class ChatListViewModel @Inject constructor( fun handleIntent(intent: ChatListIntent) { when (intent) { is ChatListIntent.ClickChatRoom -> emitNavigateToChatRoom(intent.roomId) + ChatListIntent.ClickManagerChatRoom -> TODO() } } @@ -55,7 +56,8 @@ class ChatListViewModel @Inject constructor( postSideEffect(ChatListSideEffect.NavigateToChatRoom(roomId)) } - - + private fun emitNavigateToManagerChatRoom() = intent { + postSideEffect(ChatListSideEffect.NavigateToManagerChatRoom) + } } \ No newline at end of file From e5b7e244fe8fb4d534ea364399d139d35d7cae38 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 01:51:09 +0900 Subject: [PATCH 11/26] =?UTF-8?q?refactor:=20=EB=A7=A4=EB=8B=88=EC=A0=80?= =?UTF-8?q?=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=ED=95=98=EB=8B=A8=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerChatRoomScreen: 레이아웃 구조를 Box로 변경하고 하단에 ManagerPromptSlide와 ManagerChatField를 배치 - ManagerPromptSlide: Composable에 modifier 파라미터를 추가하고 그림자(shadow) 고도를 4.dp로 조정 - ManagerChatField: 컴포넌트 시각적 구분을 위해 4.dp 크기의 shadow 효과 적용 - ManagerChatLazyColumn: 일반 메시지 중 "전화왔어" 키워드에 대해 AnnotatedString을 사용하여 강조 색상(MainVariant1) 적용 --- .../chatroom/manager/ManagerChatField.kt | 5 ++ .../chatroom/manager/ManagerChatLazyColumn.kt | 29 ++++++++++- .../chatroom/manager/ManagerPromptSlide.kt | 5 +- .../call/impl/screen/ManagerChatRoomScreen.kt | 52 ++++++++++++++----- 4 files changed, 76 insertions(+), 15 deletions(-) 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 index 500707d..632a454 100644 --- 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 @@ -6,6 +6,7 @@ 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 @@ -22,6 +23,10 @@ fun ManagerChatField( Box( modifier = modifier .fillMaxWidth() + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(50) + ) .height(44.dp) .background( color = CallTheme.colors.subGray, 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 index 534690c..80ffb3f 100644 --- 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 @@ -9,6 +9,10 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable 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 kr.co.call.designsystem.theme.MainVariant1 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import java.time.LocalDateTime @@ -44,8 +48,31 @@ fun ManagerChatLazyColumn( when (message.type) { ManagerFirstMessageType.NORMAL -> { + // "전화왔어"가 포함된 경우 해당 단어만 브랜드 컬러로 강조 + val highlightWord = "전화왔어" + + val annotated = buildAnnotatedString { + // 강조할 키워드의 시작 위치 + val startIndex = message.content.indexOf(highlightWord) + + if (startIndex >= 0) { + // 키워드 앞부분은 기본 스타일로 추가 + append(message.content.substring(0, startIndex)) + + // 키워드에만 강조 색상을 적용 + withStyle(SpanStyle(color = MainVariant1)) { + append(highlightWord) + } + + // 키워드 이후 문자열을 이어서 추가 + append(message.content.substring(startIndex + highlightWord.length)) + } else { + // 키워드가 없는 경우 원본 문자열을 그대로 표시 + append(message.content) + } + } ChatGrayBubble( - text = AnnotatedString(message.content), + text = annotated, time = item.time, isLoading = item.loadStatus == LoadStatus.Loading ) 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 index 59ffd24..3e79758 100644 --- 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 @@ -38,6 +38,7 @@ import kr.co.call.designsystem.theme.CallTheme @Composable fun ManagerPromptSlide( + modifier: Modifier = Modifier, onCallTimeClick: () -> Unit = {}, onChangePartnerClick: () -> Unit = {}, onEditRecordClick: () -> Unit = {}, @@ -46,10 +47,10 @@ fun ManagerPromptSlide( var isExpanded by remember { mutableStateOf(false) } Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .shadow( - elevation = 8.dp, + elevation = 4.dp, shape = RoundedCornerShape(30.dp), ) .background(Color.White, RoundedCornerShape(30.dp)) 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 index d75842e..5e79945 100644 --- 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 @@ -1,12 +1,15 @@ 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.ui.Alignment import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -17,8 +20,10 @@ 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.viewmodel.ManagerChatRoomUiState import kr.co.call.impl.viewmodel.ManagerChatRoomViewModel import kr.co.call.impl.viewmodel.ManagerChatUiItem @@ -46,25 +51,48 @@ fun ManagerChatRoomScreenContent( onBack: () -> Unit, state: ManagerChatRoomUiState, ) { - Column( + Box( modifier = modifier .fillMaxSize() .background(CallTheme.colors.mainVariant5Chat) .statusBarsPadding() ) { - Spacer(Modifier.height(8.dp)) + Column(modifier = Modifier.fillMaxSize()) { + Spacer(Modifier.height(8.dp)) - ManagerChatTopBar( - modifier = Modifier.fillMaxWidth(), - onBack = onBack - ) + ManagerChatTopBar( + modifier = Modifier.fillMaxWidth(), + onBack = onBack + ) - ManagerChatLazyColumn( - modifier = Modifier.fillMaxWidth() - .weight(1f) - .background(CallTheme.colors.background), - chatItems = state.chatItems - ) + ManagerChatLazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .background(CallTheme.colors.background), + chatItems = state.chatItems + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) { + ManagerPromptSlide( + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(Modifier.height(15.dp)) + + ManagerChatField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + + Spacer(Modifier.height(15.dp)) + } } } From b2920d0da6749d1a150656f884d715315ee9b77f Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 02:15:11 +0900 Subject: [PATCH 12/26] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=95=88=EB=82=B4=20=EA=B8=B0=EB=8A=A5=20=EC=9C=A0?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=EC=B2=AB=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `GetCallScheduleUseCase`를 신규 생성하여 전화 예정 시간 안내 로직 구현 - `ManagerChatRoomViewModel`에 `GetCallScheduleUseCase` 의존성 주입 - `ManagerMessage.kt`에 전화 안내 메시지 표현을 위한 `WhenCallMessage` 데이터 클래스 추가 - `FirstManagerChatUseCase`의 기본 환영 인사말 문구를 매니저 정체성이 드러나도록 수정 - `ManagerMessage.kt` 내 가독성을 위한 주석 추가 및 코드 구조 정리 --- .../domain/model/chatting/ManagerMessage.kt | 10 ++++- .../chatting/FirstManagerChatUseCase.kt | 2 +- .../chatting/GetCallScheduleUseCase.kt | 40 +++++++++++++++++++ .../viewmodel/ManagerChatRoomViewModel.kt | 4 +- 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 core/domain/src/main/java/kr/co/call/domain/usecase/chatting/GetCallScheduleUseCase.kt diff --git a/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt index 4f2f63c..83eba14 100644 --- a/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt +++ b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt @@ -7,6 +7,7 @@ sealed interface ManagerChatItem { val createdAt: LocalDateTime } +// ======== 첫 번째 매니저 채팅 ========== data class ManagerFirstMessage( override val id: String, val content: String, @@ -17,4 +18,11 @@ data class ManagerFirstMessage( enum class ManagerFirstMessageType { NORMAL, RELATIONSHIP // 대화를 많이 나눌수록, 더 자연스러운 관계가 생성돼요. -} \ No newline at end of file +} + +// ======= 전화는 언제 오나요? 에 대한 매니저 채팅 ========== +data class WhenCallMessage( + override val id: String, + val content: String, + override val createdAt: LocalDateTime, +): ManagerChatItem \ No newline at end of file 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 index 9b59607..a0bcaee 100644 --- 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 @@ -30,7 +30,7 @@ class FirstManagerChatUseCase @Inject constructor() { emit( ManagerFirstMessage( id = "welcome", - content = "수현님, 반가워요! 👋🏻", + content = "반가워요! 👋🏻 전화왔어 매니저에요!", type = ManagerFirstMessageType.NORMAL, createdAt = now ) diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/GetCallScheduleUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/GetCallScheduleUseCase.kt new file mode 100644 index 0000000..d5018fe --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/GetCallScheduleUseCase.kt @@ -0,0 +1,40 @@ +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 javax.inject.Inject + +/** + * 사용자가 "전화는 언제 오나요?" 프롬프트를 선택했을 때 + * 매니저가 제공하는 전화 예정 시간 안내 메시지를 제공합니다. + */ +class GetCallScheduleUseCase @Inject constructor() { + + /** + * 전화 예정 시간 관련 매니저 메시지를 반환합니다. + * + * @return 전화 일정 안내 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + WhenCallMessage( + id = "call_schedule", + content = "설정한 시간대를 기준으로 먼저 전화가 와요 ☎️", + createdAt = now + ) + ) + + emit( + WhenCallMessage( + id = "call_schedule_setting", + content = "원하는 시간은 마이페이지 > 연락시간 설정에서\n언제든지 변경할 수 있어요.", + createdAt = now + ) + ) + } +} \ No newline at end of file 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 index 8ae3b94..75c8a46 100644 --- 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kr.co.call.domain.usecase.chatting.FirstManagerChatUseCase +import kr.co.call.domain.usecase.chatting.GetCallScheduleUseCase import kr.co.call.domain.util.LoadStatus import kr.co.call.impl.mapper.UiModelMapper.toUiItem import org.orbitmvi.orbit.Container @@ -14,7 +15,8 @@ import kotlin.time.Duration.Companion.milliseconds @HiltViewModel class ManagerChatRoomViewModel @Inject constructor( - private val firstManagerChatUseCase: FirstManagerChatUseCase + private val firstManagerChatUseCase: FirstManagerChatUseCase, + private val getCallScheduleUseCase: GetCallScheduleUseCase, ): ViewModel(), ContainerHost { override val container: Container = container( From bd4d029d05020d85d3199920cfa3864852906a55 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 03:14:44 +0900 Subject: [PATCH 13/26] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EA=B0=81=20=EC=9C=A0=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EB=93=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/data/di/UseCaseModule.kt | 30 ++++++++ .../domain/model/chatting/ManagerChatItem.kt | 73 +++++++++++++++++++ .../domain/model/chatting/ManagerMessage.kt | 28 ------- .../chatting/WantToContactManagerUseCase.kt | 32 ++++++++ ...ase.kt => WantToGetCallScheduleUseCase.kt} | 2 +- .../WantToUpdatePartnerInfoUseCase.kt | 52 +++++++++++++ .../chatting/WantToUpdateRecordUseCase.kt | 43 +++++++++++ .../viewmodel/ManagerChatRoomViewModel.kt | 4 +- 8 files changed, 233 insertions(+), 31 deletions(-) create mode 100644 core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerChatItem.kt delete mode 100644 core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToContactManagerUseCase.kt rename core/domain/src/main/java/kr/co/call/domain/usecase/chatting/{GetCallScheduleUseCase.kt => WantToGetCallScheduleUseCase.kt} (95%) create mode 100644 core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdatePartnerInfoUseCase.kt create mode 100644 core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdateRecordUseCase.kt 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 a733bd6..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 @@ -5,15 +5,45 @@ 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/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..6acffb6 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerChatItem.kt @@ -0,0 +1,73 @@ +package kr.co.call.domain.model.chatting + +import java.time.LocalDateTime + +sealed interface ManagerChatItem { + val id: String + val createdAt: LocalDateTime +} + +// ======== 첫 번째 매니저 채팅 ========== +data class ManagerFirstMessage( + override val id: String, + val content: String, + val type: ManagerFirstMessageType, + override val createdAt: LocalDateTime, +): ManagerChatItem + +enum class ManagerFirstMessageType { + NORMAL, + RELATIONSHIP // 대화를 많이 나눌수록, 더 자연스러운 관계가 생성돼요. +} + + +// ======= 전화는 언제 오나요? 에 대한 매니저 채팅 ========== +data class WhenCallMessage( + override val id: String, + val content: String, + override val createdAt: LocalDateTime, +): ManagerChatItem + + +// ======= 상대방 정보를 변경하고 싶어요. 에 대한 매니저 채팅 ========== +data class ChangePartnerInfoMessage( + override val id: String, + val content: String, + val type: ChangePartnerInfoMessageType, + override val createdAt: LocalDateTime, +): ManagerChatItem + +enum class ChangePartnerInfoMessageType { + NORMAL, + NEXT_CONVERSATION // 변경한 설정은 다음 대화와 전화부터 바로 반영 돼요. +} + + +// ======= 기록한 정보를 수정하고 싶어요. 에 대한 매니저 채팅 ========== +data class UpdateInfoMessage( + override val id: String, + val content: String, + val type: UpdateInfoMessageType, + override val createdAt: LocalDateTime, +): ManagerChatItem + +enum class UpdateInfoMessageType { + NORMAL, + APPLIED_NATURALLY // 수정한 정보는 이후 대화에 자연스럽게 반영 돼요 ! +} + + +// ======= 상담원에게 문의하기 에 대한 매니저 채팅 ========== +data class AskToAgentMessage( + override val id: String, + val content: String, + override val createdAt: LocalDateTime, +): ManagerChatItem + + +// ======== 유저 채팅 ========= +data class UserMessage( + override val id: String, + val content: String, + override val createdAt: LocalDateTime, +): ManagerChatItem diff --git a/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt b/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt deleted file mode 100644 index 83eba14..0000000 --- a/core/domain/src/main/java/kr/co/call/domain/model/chatting/ManagerMessage.kt +++ /dev/null @@ -1,28 +0,0 @@ -package kr.co.call.domain.model.chatting - -import java.time.LocalDateTime - -sealed interface ManagerChatItem { - val id: String - val createdAt: LocalDateTime -} - -// ======== 첫 번째 매니저 채팅 ========== -data class ManagerFirstMessage( - override val id: String, - val content: String, - val type: ManagerFirstMessageType, - override val createdAt: LocalDateTime, -): ManagerChatItem - -enum class ManagerFirstMessageType { - NORMAL, - RELATIONSHIP // 대화를 많이 나눌수록, 더 자연스러운 관계가 생성돼요. -} - -// ======= 전화는 언제 오나요? 에 대한 매니저 채팅 ========== -data class WhenCallMessage( - override val id: String, - val content: String, - override val createdAt: LocalDateTime, -): ManagerChatItem \ 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..35ce0d5 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToContactManagerUseCase.kt @@ -0,0 +1,32 @@ +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 javax.inject.Inject + +/** + * 사용자가 "상담원에게 문의하기" 프롬프트를 선택했을 때 + * 매니저가 제공하는 안내 메시지를 제공합니다. + */ +class WantToContactManagerUseCase @Inject constructor() { + + /** + * 상담원 문의 관련 안내 메시지를 반환합니다. + * + * @return 상담원 문의 안내 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + AskToAgentMessage( + id = "ask_to_agent", + content = "상담원 문의하기 기능은 준비중이에요.", + createdAt = now + ) + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/GetCallScheduleUseCase.kt b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToGetCallScheduleUseCase.kt similarity index 95% rename from core/domain/src/main/java/kr/co/call/domain/usecase/chatting/GetCallScheduleUseCase.kt rename to core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToGetCallScheduleUseCase.kt index d5018fe..f14db6d 100644 --- a/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/GetCallScheduleUseCase.kt +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToGetCallScheduleUseCase.kt @@ -11,7 +11,7 @@ import javax.inject.Inject * 사용자가 "전화는 언제 오나요?" 프롬프트를 선택했을 때 * 매니저가 제공하는 전화 예정 시간 안내 메시지를 제공합니다. */ -class GetCallScheduleUseCase @Inject constructor() { +class WantToGetCallScheduleUseCase @Inject constructor() { /** * 전화 예정 시간 관련 매니저 메시지를 반환합니다. 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..2453524 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdatePartnerInfoUseCase.kt @@ -0,0 +1,52 @@ +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 javax.inject.Inject + +/** + * 사용자가 "상대방 정보를 수정하고 싶어요" 프롬프트를 선택했을 때 + * 매니저가 제공하는 이상형 정보 수정 안내 메시지를 제공합니다. + */ +class WantToUpdatePartnerInfoUseCase @Inject constructor() { + + /** + * 상대방(이상형) 정보 수정 관련 안내 메시지를 반환합니다. + * + * @return 이상형 정보 수정 안내 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + ChangePartnerInfoMessage( + id = "update_partner_info", + content = "상대방 정보는 마이페이지 > 이상형 정보 수정에서\n변경할 수 있어요. 👤", + type = ChangePartnerInfoMessageType.NORMAL, + createdAt = now + ) + ) + + emit( + ChangePartnerInfoMessage( + id = "update_partner_info_detail", + content = "이름, 성격, 관계 설정까지 언제든 수정 가능해요!", + type = ChangePartnerInfoMessageType.NORMAL, + createdAt = now + ) + ) + + emit( + ChangePartnerInfoMessage( + id = "update_partner_info_apply", + 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..9d13707 --- /dev/null +++ b/core/domain/src/main/java/kr/co/call/domain/usecase/chatting/WantToUpdateRecordUseCase.kt @@ -0,0 +1,43 @@ +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 javax.inject.Inject + +/** + * 사용자가 "기록한 정보를 수정하고 싶어요" 프롬프트를 선택했을 때 + * 매니저가 제공하는 내 프로필 정보 수정 안내 메시지를 제공합니다. + */ +class WantToUpdateRecordUseCase @Inject constructor() { + + /** + * 기록한 정보(내 프로필) 수정 관련 안내 메시지를 반환합니다. + * + * @return 기록 정보 수정 안내 메시지 스트림 + */ + operator fun invoke(): Flow = flow { + val now = LocalDateTime.now() + + emit( + UpdateInfoMessage( + id = "update_record", + content = "기록한 정보는 마이페이지 > 내 프로필에서\n수정할 수 있어요. 📝", + type = UpdateInfoMessageType.NORMAL, + createdAt = now + ) + ) + + emit( + UpdateInfoMessage( + id = "update_record_apply", + content = "수정한 정보는\n이후 대화에 자연스럽게 반영 돼요!", + type = UpdateInfoMessageType.APPLIED_NATURALLY, + createdAt = now + ) + ) + } +} \ No newline at end of file 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 index 75c8a46..a471b59 100644 --- 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 @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kr.co.call.domain.usecase.chatting.FirstManagerChatUseCase -import kr.co.call.domain.usecase.chatting.GetCallScheduleUseCase +import kr.co.call.domain.usecase.chatting.WantToGetCallScheduleUseCase import kr.co.call.domain.util.LoadStatus import kr.co.call.impl.mapper.UiModelMapper.toUiItem import org.orbitmvi.orbit.Container @@ -16,7 +16,7 @@ import kotlin.time.Duration.Companion.milliseconds @HiltViewModel class ManagerChatRoomViewModel @Inject constructor( private val firstManagerChatUseCase: FirstManagerChatUseCase, - private val getCallScheduleUseCase: GetCallScheduleUseCase, + private val wantToGetCallScheduleUseCase: WantToGetCallScheduleUseCase ): ViewModel(), ContainerHost { override val container: Container = container( From 0cf32b49f06852d988f29946529eaa3b7256b176 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 03:20:04 +0900 Subject: [PATCH 14/26] =?UTF-8?q?chore:=20intent,=20state,=20sideeffect=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=8B=A0=EC=84=A4=20=ED=9B=84=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/call/impl/{viewmodel => Intent}/ChatListIntent.kt | 2 +- .../src/main/java/kr/co/call/impl/Intent/ChatRoomIntent.kt | 4 ++++ .../component/chatroom/manager/ManagerChatLazyColumn.kt | 2 +- .../src/main/java/kr/co/call/impl/mapper/UiModelMapper.kt | 2 +- .../co/call/impl/{viewmodel => model}/ManagerChatUiItem.kt | 2 +- .../src/main/java/kr/co/call/impl/screen/ChatListScreen.kt | 6 +++--- .../java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt | 4 ++-- .../impl/{viewmodel => sideeffect}/ChatListSideEffect.kt | 4 ++-- .../impl/{viewmodel => sideeffect}/ChatRoomSideEffect.kt | 2 +- .../kr/co/call/impl/{viewmodel => state}/ChatListState.kt | 2 +- .../kr/co/call/impl/{viewmodel => state}/ChatRoomUiState.kt | 3 +-- .../impl/{viewmodel => state}/ManagerChatRoomUiState.kt | 5 +++-- .../java/kr/co/call/impl/viewmodel/ChatListViewModel.kt | 3 +++ .../main/java/kr/co/call/impl/viewmodel/ChatRoomIntent.kt | 4 ---- .../java/kr/co/call/impl/viewmodel/ChatRoomViewModel.kt | 2 ++ .../kr/co/call/impl/viewmodel/ManagerChatRoomViewModel.kt | 1 + 16 files changed, 27 insertions(+), 21 deletions(-) rename feature/chatting/impl/src/main/java/kr/co/call/impl/{viewmodel => Intent}/ChatListIntent.kt (82%) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/Intent/ChatRoomIntent.kt rename feature/chatting/impl/src/main/java/kr/co/call/impl/{viewmodel => model}/ManagerChatUiItem.kt (87%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/{viewmodel => sideeffect}/ChatListSideEffect.kt (82%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/{viewmodel => sideeffect}/ChatRoomSideEffect.kt (53%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/{viewmodel => state}/ChatListState.kt (86%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/{viewmodel => state}/ChatRoomUiState.kt (94%) rename feature/chatting/impl/src/main/java/kr/co/call/impl/{viewmodel => state}/ManagerChatRoomUiState.kt (54%) delete mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatRoomIntent.kt 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/Intent/ChatListIntent.kt similarity index 82% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListIntent.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/Intent/ChatListIntent.kt index 44b19d6..1e41811 100644 --- 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/Intent/ChatListIntent.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.viewmodel +package kr.co.call.impl.Intent sealed interface ChatListIntent { data class ClickChatRoom(val roomId: Long) : ChatListIntent 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..d5d63ce --- /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 + +interface ChatRoomIntent { +} \ 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 index 80ffb3f..866322f 100644 --- 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 @@ -21,7 +21,7 @@ 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.ChatGrayBubble -import kr.co.call.impl.viewmodel.ManagerChatUiItem +import kr.co.call.impl.model.ManagerChatUiItem @Composable fun ManagerChatLazyColumn( 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 c3524a0..797c95b 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 @@ -7,7 +7,7 @@ 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.viewmodel.ManagerChatUiItem +import kr.co.call.impl.model.ManagerChatUiItem import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.UUID diff --git a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatUiItem.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/model/ManagerChatUiItem.kt similarity index 87% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatUiItem.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/model/ManagerChatUiItem.kt index 39609a6..4932269 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatUiItem.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/model/ManagerChatUiItem.kt @@ -1,4 +1,4 @@ -package kr.co.call.impl.viewmodel +package kr.co.call.impl.model import kr.co.call.domain.model.chatting.ManagerChatItem import kr.co.call.domain.util.LoadStatus 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 2f5c6cf..47ed5fd 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 @@ -22,9 +22,9 @@ import kr.co.call.domain.util.LoadStatus 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.viewmodel.ChatListIntent -import kr.co.call.impl.viewmodel.ChatListSideEffect -import kr.co.call.impl.viewmodel.ChatListState +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 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 index 5e79945..bedd7c3 100644 --- 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 @@ -24,9 +24,9 @@ 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.viewmodel.ManagerChatRoomUiState +import kr.co.call.impl.state.ManagerChatRoomUiState import kr.co.call.impl.viewmodel.ManagerChatRoomViewModel -import kr.co.call.impl.viewmodel.ManagerChatUiItem +import kr.co.call.impl.model.ManagerChatUiItem import org.orbitmvi.orbit.compose.collectAsState @Composable 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/sideeffect/ChatListSideEffect.kt similarity index 82% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListSideEffect.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/sideeffect/ChatListSideEffect.kt index 1873fbf..eca4cc2 100644 --- 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/sideeffect/ChatListSideEffect.kt @@ -1,6 +1,6 @@ -package kr.co.call.impl.viewmodel +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/viewmodel/ManagerChatRoomUiState.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ManagerChatRoomUiState.kt similarity index 54% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomUiState.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/state/ManagerChatRoomUiState.kt index 5312621..b05e62a 100644 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ManagerChatRoomUiState.kt +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/state/ManagerChatRoomUiState.kt @@ -1,6 +1,7 @@ -package kr.co.call.impl.viewmodel +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/ChatListViewModel.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/viewmodel/ChatListViewModel.kt index 1acc9e7..7506c42 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 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 index a471b59..da08880 100644 --- 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 @@ -7,6 +7,7 @@ import kr.co.call.domain.usecase.chatting.FirstManagerChatUseCase import kr.co.call.domain.usecase.chatting.WantToGetCallScheduleUseCase import kr.co.call.domain.util.LoadStatus import kr.co.call.impl.mapper.UiModelMapper.toUiItem +import kr.co.call.impl.state.ManagerChatRoomUiState import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container From 5d5641b4f86cf9f0bd6f2588f7878c096edfec32 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 03:56:02 +0900 Subject: [PATCH 15/26] =?UTF-8?q?refactor:=20intent=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B2=BD=EB=A1=9C=20=EB=8C=80=EC=86=8C=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A7=A4=EB=8B=88?= =?UTF-8?q?=EC=A0=80=20=EC=B1=84=ED=8C=85=20=EB=B7=B0=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일관성을 위해 Intent 패키지를 `Intent`에서 `intent`(소문자)로 변경 - ChatListViewModel의 import를 `impl.Intent.ChatListIntent`에서 `impl.intent.ChatListIntent`로 업데이트 - ChatListIntent.kt와 ChatRoomIntent.kt를 올바른 패키지 경로로 이동 - AnnotatedString 스타일 변경 사항을 반영하도록 ManagerChatLazyColumn 업데이트 --- .../kr/co/call/impl/Intent/ChatRoomIntent.kt | 4 - .../chatroom/manager/ManagerChatLazyColumn.kt | 12 +++ .../impl/{Intent => intent}/ChatListIntent.kt | 2 +- .../kr/co/call/impl/intent/ChatRoomIntent.kt | 4 + .../call/impl/intent/ManagerChatRoomIntent.kt | 8 ++ .../kr/co/call/impl/screen/ChatListScreen.kt | 2 +- .../call/impl/viewmodel/ChatListViewModel.kt | 2 +- .../viewmodel/ManagerChatRoomViewModel.kt | 75 +++++++++++++------ 8 files changed, 80 insertions(+), 29 deletions(-) delete mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/Intent/ChatRoomIntent.kt rename feature/chatting/impl/src/main/java/kr/co/call/impl/{Intent => intent}/ChatListIntent.kt (83%) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ChatRoomIntent.kt create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ManagerChatRoomIntent.kt 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 deleted file mode 100644 index d5d63ce..0000000 --- a/feature/chatting/impl/src/main/java/kr/co/call/impl/Intent/ChatRoomIntent.kt +++ /dev/null @@ -1,4 +0,0 @@ -package kr.co.call.impl.Intent - -interface ChatRoomIntent { -} \ 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 index 866322f..bb5345c 100644 --- 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 @@ -17,8 +17,13 @@ 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.ManagerFirstMessage import kr.co.call.domain.model.chatting.ManagerFirstMessageType +import kr.co.call.domain.model.chatting.UpdateInfoMessage +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.model.ManagerChatUiItem @@ -44,6 +49,7 @@ fun ManagerChatLazyColumn( ) { item -> when(val message = item.message) { + // 첫 번째 매니저 채팅 is ManagerFirstMessage -> { when (message.type) { @@ -88,6 +94,12 @@ fun ManagerChatLazyColumn( } } + // 상담원 문의 + is AskToAgentMessage -> TODO() + is ChangePartnerInfoMessage -> TODO() + is UpdateInfoMessage -> TODO() + is UserMessage -> TODO() + is WhenCallMessage -> TODO() } } 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 similarity index 83% rename from feature/chatting/impl/src/main/java/kr/co/call/impl/Intent/ChatListIntent.kt rename to feature/chatting/impl/src/main/java/kr/co/call/impl/intent/ChatListIntent.kt index 1e41811..f6585fb 100644 --- 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 @@ -1,4 +1,4 @@ -package kr.co.call.impl.Intent +package kr.co.call.impl.intent sealed interface ChatListIntent { data class ClickChatRoom(val roomId: Long) : ChatListIntent 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/screen/ChatListScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ChatListScreen.kt index 47ed5fd..3f9a172 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 @@ -22,7 +22,7 @@ import kr.co.call.domain.util.LoadStatus 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.intent.ChatListIntent import kr.co.call.impl.sideeffect.ChatListSideEffect import kr.co.call.impl.state.ChatListState import kr.co.call.impl.viewmodel.ChatListViewModel 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 7506c42..de87fff 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,7 +4,7 @@ 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.intent.ChatListIntent import kr.co.call.impl.sideeffect.ChatListSideEffect import kr.co.call.impl.state.ChatListState import org.orbitmvi.orbit.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 index da08880..620e606 100644 --- 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 @@ -3,21 +3,35 @@ package kr.co.call.impl.viewmodel import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel 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.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.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 wantToGetCallScheduleUseCase: WantToGetCallScheduleUseCase, + private val wantToUpdatePartnerInfoUseCase: WantToUpdatePartnerInfoUseCase, + private val wantToUpdateRecordUseCase: WantToUpdateRecordUseCase, + private val wantToContactManagerUseCase: WantToContactManagerUseCase ): ViewModel(), ContainerHost { override val container: Container = container( @@ -25,36 +39,53 @@ class ManagerChatRoomViewModel @Inject constructor( ) init { - collectFirstMessages() + intent { + appendManagerMessages(firstManagerChatUseCase()) + } } - // 초기 매니저 메시지를 순차적으로 수집하여 채팅 목록에 추가 - // 각 메시지는 먼저 로딩 상태로 노출한 뒤, 일정 시간이 지나면 로딩을 해제하여 실제 메시지 내용을 표시 - private fun collectFirstMessages() = intent { - firstManagerChatUseCase().collect { message -> + fun handleIntent(intent: ManagerChatRoomIntent) { + when (intent) { + ManagerChatRoomIntent.ClickWhenCall -> + sendUserMessageThen("전화는 언제 오나요?", wantToGetCallScheduleUseCase()) + ManagerChatRoomIntent.ClickChangePartnerInfo -> + sendUserMessageThen("상대방 정보를 변경하고 싶어요.", wantToUpdatePartnerInfoUseCase()) + ManagerChatRoomIntent.ClickUpdateInfo -> + sendUserMessageThen("기록한 정보를 수정하고 싶어요.", wantToUpdateRecordUseCase()) + ManagerChatRoomIntent.ClickAskToAgent -> + sendUserMessageThen("상담원에게 문의하기", wantToContactManagerUseCase()) + } + } + + private fun sendUserMessageThen(content: String, flow: Flow) = intent { + val userMessage = UserMessage( + id = UUID.randomUUID().toString(), + content = content, + createdAt = LocalDateTime.now() + ) + reduce { + state.copy(chatItems = state.chatItems + userMessage.toUiItem()) + } + appendManagerMessages(flow) + } - // 로딩 상태의 메시지를 채팅 목록에 추가 + // 메시지를 로딩 상태로 추가한 뒤, 일정 시간 후 실제 내용을 표시하는 공통 로직 + // + // intent 블록의 리시버가 Syntax이고 reduce가 그 멤버이므로, + // 이 함수도 Syntax 확장 함수로 선언해야 내부에서 reduce를 호출할 수 있다. + private suspend fun Syntax.appendManagerMessages( + flow: Flow + ) { + flow.collect { message -> reduce { - state.copy( - chatItems = state.chatItems + message.toUiItem( - loadStatus = LoadStatus.Loading - ) - ) + state.copy(chatItems = state.chatItems + message.toUiItem(loadStatus = LoadStatus.Loading)) } - - // 매니저가 입력 중인 것처럼 보이도록 일정 시간 대기 delay(1500.milliseconds) - - // 동일한 메시지의 로딩 상태를 해제하여 실제 내용을 표시 reduce { state.copy( chatItems = state.chatItems.map { item -> - // 현재 처리 중인 메시지라면 로딩 상태를 해제 - if (item.message.id == message.id) { - item.copy(loadStatus = LoadStatus.Idle) - } else { - item - } + if (item.message.id == message.id) item.copy(loadStatus = LoadStatus.Idle) + else item } ) } From 2544d67ef81c29d69e2300a7a2d768011909926f Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 04:39:42 +0900 Subject: [PATCH 16/26] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20Intent=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EB=B3=84=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerChatRoomScreen: Intent 처리를 위한 `onIntent` 콜백 추가 및 ViewModel의 `handleIntent` 연결 - ManagerPromptSlide: 개별 클릭 콜백을 `ManagerChatRoomIntent`를 사용하는 단일 `onIntent`로 통합 - ManagerChatLazyColumn: `TODO()`로 남겨져 있던 다양한 메시지 타입(상담원 문의, 정보 변경, 기록 수정, 유저 메시지 등)에 대한 UI 렌더링 로직 구현 - ManagerChatRoomViewModel: `UserMessage` 생성 시 UI 구분을 위한 임시 ID 생성 로직에 주석 추가 --- .../chatroom/manager/ManagerChatLazyColumn.kt | 75 +++++++++++++++++-- .../chatroom/manager/ManagerPromptSlide.kt | 16 ++-- .../call/impl/screen/ManagerChatRoomScreen.kt | 12 ++- .../viewmodel/ManagerChatRoomViewModel.kt | 2 +- 4 files changed, 87 insertions(+), 18 deletions(-) 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 index bb5345c..6ab8c87 100644 --- 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 @@ -2,6 +2,8 @@ package kr.co.call.impl.component.chatroom.manager import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items @@ -19,13 +21,16 @@ 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 @Composable @@ -95,11 +100,71 @@ fun ManagerChatLazyColumn( } // 상담원 문의 - is AskToAgentMessage -> TODO() - is ChangePartnerInfoMessage -> TODO() - is UpdateInfoMessage -> TODO() - is UserMessage -> TODO() - is WhenCallMessage -> TODO() + 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 = AnnotatedString(message.content), + 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 = AnnotatedString(message.content), + time = item.time + ) + } + } + } + + is UserMessage -> { + Row( + modifier = Modifier.fillMaxWidth(), + 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 + ) + } } } 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 index 3e79758..e447786 100644 --- 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 @@ -35,14 +35,12 @@ 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 @Composable fun ManagerPromptSlide( + onIntent: (ManagerChatRoomIntent) -> Unit, modifier: Modifier = Modifier, - onCallTimeClick: () -> Unit = {}, - onChangePartnerClick: () -> Unit = {}, - onEditRecordClick: () -> Unit = {}, - onInquiryClick: () -> Unit = {} ) { var isExpanded by remember { mutableStateOf(false) } @@ -76,8 +74,8 @@ fun ManagerPromptSlide( Spacer(modifier = Modifier.height(21.dp)) // 기본 노출 항목 - PromptItem(emoji = "☎️", text = "전화는 언제 오나요?", onClick = onCallTimeClick) - PromptItem(emoji = "👤", text = "상대방 정보를 변경하고 싶어요.", onClick = onChangePartnerClick) + PromptItem(emoji = "☎️", text = "전화는 언제 오나요?", onClick = { onIntent(ManagerChatRoomIntent.ClickWhenCall) }) + PromptItem(emoji = "👤", text = "상대방 정보를 변경하고 싶어요.", onClick = { onIntent(ManagerChatRoomIntent.ClickChangePartnerInfo) }) // 슬라이드 업 시 추가 노출 항목 AnimatedVisibility( @@ -86,8 +84,8 @@ fun ManagerPromptSlide( exit = shrinkVertically(animationSpec = tween(300)) ) { Column { - PromptItem(emoji = "📝", text = "기록한 정보를 수정하고 싶어요.", onClick = onEditRecordClick) - PromptItem(emoji = "💬", text = "상담원에게 문의하기", onClick = onInquiryClick) + PromptItem(emoji = "📝", text = "기록한 정보를 수정하고 싶어요.", onClick = { onIntent(ManagerChatRoomIntent.ClickUpdateInfo) }) + PromptItem(emoji = "💬", text = "상담원에게 문의하기", onClick = { onIntent(ManagerChatRoomIntent.ClickAskToAgent) }) } } } @@ -125,6 +123,6 @@ private fun PromptItem( @Composable private fun ManagerPromptSlidePreview() { CallFromAiTheme { - ManagerPromptSlide() + ManagerPromptSlide(onIntent = {}) } } 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 index bedd7c3..cd710fe 100644 --- 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 @@ -9,10 +9,11 @@ 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.ui.Alignment import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview + import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import java.time.LocalDateTime @@ -24,6 +25,7 @@ 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 @@ -41,7 +43,8 @@ fun ManagerChatRoomScreen( ManagerChatRoomScreenContent( modifier = modifier, onBack = onBack, - state = state // 상태 호이스팅 + state = state, // 상태 호이스팅 + onIntent = viewModel::handleIntent ) } @@ -50,6 +53,7 @@ fun ManagerChatRoomScreenContent( modifier: Modifier = Modifier, onBack: () -> Unit, state: ManagerChatRoomUiState, + onIntent: (ManagerChatRoomIntent) -> Unit ) { Box( modifier = modifier @@ -80,6 +84,7 @@ fun ManagerChatRoomScreenContent( .align(Alignment.BottomCenter) ) { ManagerPromptSlide( + onIntent = onIntent, modifier = Modifier.padding(horizontal = 16.dp) ) @@ -114,7 +119,8 @@ private fun ManagerChatRoomScreenContentPreview() { ) ) ), - onBack = {} + onBack = {}, + onIntent = {} ) } } 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 index 620e606..bc08d94 100644 --- 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 @@ -59,7 +59,7 @@ class ManagerChatRoomViewModel @Inject constructor( private fun sendUserMessageThen(content: String, flow: Flow) = intent { val userMessage = UserMessage( - id = UUID.randomUUID().toString(), + id = UUID.randomUUID().toString(), // UI 상태에서 메시지를 구분하기 위한 임시 ID 생성 content = content, createdAt = LocalDateTime.now() ) From 10beb9ea70630cd6e2f271c8944799438f1160dd Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 05:21:56 +0900 Subject: [PATCH 17/26] =?UTF-8?q?=E2=9C=A8=20feat(ui):=20=ED=95=98?= =?UTF-8?q?=EB=8B=A8=20=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=95=98=EB=8B=A8=20=ED=8C=A8=EB=94=A9=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerChatRoomScreen: onSizeChanged를 사용하여 하단 프롬프트 영역의 실제 높이를 측정하는 로직 추가 - ManagerChatRoomScreen: 측정된 높이를 ManagerChatLazyColumn의 bottomPadding으로 전달하여 UI 겹침 방지 - ManagerChatRoomScreen: 스크롤 및 레이아웃 테스트를 위한 대량의 더미 데이터를 포함한 Preview 추가 - ManagerChatLazyColumn: bottomPadding 매개변수를 추가하고 LazyColumn의 하단 contentPadding에 반영하도록 수정 --- .../chatroom/manager/ManagerChatLazyColumn.kt | 9 ++-- .../call/impl/screen/ManagerChatRoomScreen.kt | 42 +++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) 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 index 6ab8c87..37ff975 100644 --- 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 @@ -16,6 +16,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import kr.co.call.designsystem.theme.MainVariant1 import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import java.time.LocalDateTime import kr.co.call.designsystem.theme.CallFromAiTheme @@ -38,15 +39,17 @@ 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( - horizontal = 16.dp, - vertical = 13.dp - ) + start = 16.dp, end = 16.dp, + top = 13.dp, + bottom = 13.dp + bottomPadding, // 오버레이 높이만큼 확보 + ), ) { items( items = chatItems, 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 index cd710fe..8b029d2 100644 --- 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 @@ -10,8 +10,14 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding 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.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -55,6 +61,9 @@ fun ManagerChatRoomScreenContent( state: ManagerChatRoomUiState, onIntent: (ManagerChatRoomIntent) -> Unit ) { + val density = LocalDensity.current + var overlayHeight by remember { mutableStateOf(0.dp) } + Box( modifier = modifier .fillMaxSize() @@ -71,10 +80,10 @@ fun ManagerChatRoomScreenContent( ManagerChatLazyColumn( modifier = Modifier - .fillMaxWidth() - .weight(1f) + .fillMaxSize() .background(CallTheme.colors.background), - chatItems = state.chatItems + chatItems = state.chatItems, + bottomPadding = overlayHeight, // 측정한 높이 주입 ) } @@ -82,6 +91,9 @@ fun ManagerChatRoomScreenContent( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) + .onSizeChanged { // 오버레이 실제 높이 측정 + overlayHeight = with(density) { it.height.toDp() } + } ) { ManagerPromptSlide( onIntent = onIntent, @@ -125,4 +137,28 @@ private fun ManagerChatRoomScreenContentPreview() { } } +@Preview(showBackground = true) +@Composable +private fun ManagerChatRoomScreenContentTestPreview() { + CallFromAiTheme { + ManagerChatRoomScreenContent( + state = ManagerChatRoomUiState( + chatItems = List(40) { i -> + ManagerChatUiItem( + message = ManagerFirstMessage( + id = "$i", // 유니크해야 함 + content = "메시지 $i - 스크롤 테스트용 더미 데이터입니다.", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + time = "오전 10:${(i % 60).toString().padStart(2, '0')}" + ) + } + ), + onBack = {}, + onIntent = {} + ) + } +} + From e6f562b72d44e5a9d87993c2198d07f92e14e1a7 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 05:38:38 +0900 Subject: [PATCH 18/26] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usecase/chatting/FirstManagerChatUseCase.kt | 9 +++++---- .../chatting/WantToContactManagerUseCase.kt | 3 ++- .../chatting/WantToGetCallScheduleUseCase.kt | 5 +++-- .../chatting/WantToUpdatePartnerInfoUseCase.kt | 7 ++++--- .../chatting/WantToUpdateRecordUseCase.kt | 5 +++-- .../main/java/kr/co/call/api/ChattingRoute.kt | 1 + .../call/impl/screen/ManagerChatRoomScreen.kt | 17 +++++++++++++++-- 7 files changed, 33 insertions(+), 14 deletions(-) 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 index a0bcaee..7945af3 100644 --- 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 @@ -7,6 +7,7 @@ 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 @@ -29,7 +30,7 @@ class FirstManagerChatUseCase @Inject constructor() { emit( ManagerFirstMessage( - id = "welcome", + id = UUID.randomUUID().toString(), content = "반가워요! 👋🏻 전화왔어 매니저에요!", type = ManagerFirstMessageType.NORMAL, createdAt = now @@ -38,7 +39,7 @@ class FirstManagerChatUseCase @Inject constructor() { emit( ManagerFirstMessage( - id = "intro", + id = UUID.randomUUID().toString(), content = "전화왔어는 AI가 먼저 연락하고,\n사용자를 기억하며 관계를 만들어가는 서비스예요.", type = ManagerFirstMessageType.NORMAL, createdAt = now @@ -47,7 +48,7 @@ class FirstManagerChatUseCase @Inject constructor() { emit( ManagerFirstMessage( - id = "relationship", + id = UUID.randomUUID().toString(), content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", type = ManagerFirstMessageType.RELATIONSHIP, createdAt = now @@ -56,7 +57,7 @@ class FirstManagerChatUseCase @Inject constructor() { emit( ManagerFirstMessage( - id = "guide", + id = UUID.randomUUID().toString(), content = "필요한 안내는 언제든 이 채팅방에서 확인해주세요!", type = ManagerFirstMessageType.NORMAL, createdAt = now 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 index 35ce0d5..4f28ea1 100644 --- 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 @@ -5,6 +5,7 @@ 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 /** @@ -23,7 +24,7 @@ class WantToContactManagerUseCase @Inject constructor() { emit( AskToAgentMessage( - id = "ask_to_agent", + id = UUID.randomUUID().toString(), content = "상담원 문의하기 기능은 준비중이에요.", createdAt = now ) 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 index f14db6d..82f41c8 100644 --- 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 @@ -5,6 +5,7 @@ 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 /** @@ -23,7 +24,7 @@ class WantToGetCallScheduleUseCase @Inject constructor() { emit( WhenCallMessage( - id = "call_schedule", + id = UUID.randomUUID().toString(), content = "설정한 시간대를 기준으로 먼저 전화가 와요 ☎️", createdAt = now ) @@ -31,7 +32,7 @@ class WantToGetCallScheduleUseCase @Inject constructor() { emit( WhenCallMessage( - id = "call_schedule_setting", + id = UUID.randomUUID().toString(), content = "원하는 시간은 마이페이지 > 연락시간 설정에서\n언제든지 변경할 수 있어요.", createdAt = now ) 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 index 2453524..66c0071 100644 --- 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 @@ -6,6 +6,7 @@ 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 /** @@ -24,7 +25,7 @@ class WantToUpdatePartnerInfoUseCase @Inject constructor() { emit( ChangePartnerInfoMessage( - id = "update_partner_info", + id = UUID.randomUUID().toString(), content = "상대방 정보는 마이페이지 > 이상형 정보 수정에서\n변경할 수 있어요. 👤", type = ChangePartnerInfoMessageType.NORMAL, createdAt = now @@ -33,7 +34,7 @@ class WantToUpdatePartnerInfoUseCase @Inject constructor() { emit( ChangePartnerInfoMessage( - id = "update_partner_info_detail", + id = UUID.randomUUID().toString(), content = "이름, 성격, 관계 설정까지 언제든 수정 가능해요!", type = ChangePartnerInfoMessageType.NORMAL, createdAt = now @@ -42,7 +43,7 @@ class WantToUpdatePartnerInfoUseCase @Inject constructor() { emit( ChangePartnerInfoMessage( - id = "update_partner_info_apply", + id = UUID.randomUUID().toString(), content = "변경한 설정은\n다음 대화와 전화부터 바로 반영 돼요.", type = ChangePartnerInfoMessageType.NEXT_CONVERSATION, createdAt = now 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 index 9d13707..d9cbd68 100644 --- 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 @@ -6,6 +6,7 @@ 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 /** @@ -24,7 +25,7 @@ class WantToUpdateRecordUseCase @Inject constructor() { emit( UpdateInfoMessage( - id = "update_record", + id = UUID.randomUUID().toString(), content = "기록한 정보는 마이페이지 > 내 프로필에서\n수정할 수 있어요. 📝", type = UpdateInfoMessageType.NORMAL, createdAt = now @@ -33,7 +34,7 @@ class WantToUpdateRecordUseCase @Inject constructor() { emit( UpdateInfoMessage( - id = "update_record_apply", + id = UUID.randomUUID().toString(), content = "수정한 정보는\n이후 대화에 자연스럽게 반영 돼요!", type = UpdateInfoMessageType.APPLIED_NATURALLY, createdAt = now 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 1521287..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 @@ -11,4 +11,5 @@ data class ChatRoomNavKey( val roomId: Long, ): 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/screen/ManagerChatRoomScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt index 8b029d2..1ab6c1e 100644 --- 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 @@ -9,7 +9,9 @@ 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 @@ -64,6 +66,16 @@ fun ManagerChatRoomScreenContent( 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() @@ -83,7 +95,8 @@ fun ManagerChatRoomScreenContent( .fillMaxSize() .background(CallTheme.colors.background), chatItems = state.chatItems, - bottomPadding = overlayHeight, // 측정한 높이 주입 + bottomPadding = overlayHeight, // 측정한 높이 주입 + listState = listState, ) } @@ -91,7 +104,7 @@ fun ManagerChatRoomScreenContent( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) - .onSizeChanged { // 오버레이 실제 높이 측정 + .onSizeChanged {// 오버레이 실제 높이 측정 overlayHeight = with(density) { it.height.toDp() } } ) { From 5ddb2a63bcc775b87cfb2de3dedf500f23c7b361 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 18:18:40 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refactor:=20=EB=A7=A4=EB=8B=88=EC=A0=80?= =?UTF-8?q?=20=EC=B1=84=ED=8C=85=20UI=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerChatItemContent.kt: 기존 LazyColumn에 포함되어 있던 메시지 유형별 UI 렌더링 로직을 별도 컴포저블로 분리 - highlightText.kt: 텍스트 내 특정 키워드 강조를 위한 AnnotatedString 생성 로직을 유틸리티 함수로 추출 - ManagerChatLazyColumn.kt: 매니저 채팅 아이템 렌더링 로직을 ManagerChatItemContent로 위임하여 코드 간소화 - ManagerChatItem.kt: ManagerChatItem 인터페이스에 content 프로퍼티를 추가하여 하위 모델들의 구조 통일 - ManagerChatRoomViewModel.kt: handleIntent 및 메시지 전송 로직에 설명 주석 추가 및 가독성 개선 --- .../domain/model/chatting/ManagerChatItem.kt | 13 +- .../manager/ManagerChatItemContent.kt | 150 ++++++++++++++++++ .../chatroom/manager/ManagerChatLazyColumn.kt | 117 +------------- .../chatroom/manager/highlightText.kt | 49 ++++++ .../call/impl/screen/ManagerChatRoomScreen.kt | 1 - .../viewmodel/ManagerChatRoomViewModel.kt | 5 + 6 files changed, 214 insertions(+), 121 deletions(-) create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatItemContent.kt create mode 100644 feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/highlightText.kt 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 index 6acffb6..20a0126 100644 --- 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 @@ -4,13 +4,14 @@ import java.time.LocalDateTime sealed interface ManagerChatItem { val id: String + val content: String val createdAt: LocalDateTime } // ======== 첫 번째 매니저 채팅 ========== data class ManagerFirstMessage( override val id: String, - val content: String, + override val content: String, val type: ManagerFirstMessageType, override val createdAt: LocalDateTime, ): ManagerChatItem @@ -24,7 +25,7 @@ enum class ManagerFirstMessageType { // ======= 전화는 언제 오나요? 에 대한 매니저 채팅 ========== data class WhenCallMessage( override val id: String, - val content: String, + override val content: String, override val createdAt: LocalDateTime, ): ManagerChatItem @@ -32,7 +33,7 @@ data class WhenCallMessage( // ======= 상대방 정보를 변경하고 싶어요. 에 대한 매니저 채팅 ========== data class ChangePartnerInfoMessage( override val id: String, - val content: String, + override val content: String, val type: ChangePartnerInfoMessageType, override val createdAt: LocalDateTime, ): ManagerChatItem @@ -46,7 +47,7 @@ enum class ChangePartnerInfoMessageType { // ======= 기록한 정보를 수정하고 싶어요. 에 대한 매니저 채팅 ========== data class UpdateInfoMessage( override val id: String, - val content: String, + override val content: String, val type: UpdateInfoMessageType, override val createdAt: LocalDateTime, ): ManagerChatItem @@ -60,7 +61,7 @@ enum class UpdateInfoMessageType { // ======= 상담원에게 문의하기 에 대한 매니저 채팅 ========== data class AskToAgentMessage( override val id: String, - val content: String, + override val content: String, override val createdAt: LocalDateTime, ): ManagerChatItem @@ -68,6 +69,6 @@ data class AskToAgentMessage( // ======== 유저 채팅 ========= data class UserMessage( override val id: String, - val content: String, + override val content: String, override val createdAt: LocalDateTime, ): ManagerChatItem 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..c452583 --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerChatItemContent.kt @@ -0,0 +1,150 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +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 kr.co.call.designsystem.theme.MainVariant1 +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, + 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 + ) + } + } + } + + // 유저 메세지 + is UserMessage -> { + Row( + modifier = Modifier.fillMaxWidth(), + 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 + ) + } + } +} \ 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 index 37ff975..2328130 100644 --- 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 @@ -55,120 +55,9 @@ fun ManagerChatLazyColumn( items = chatItems, key = { it.message.id } ) { item -> - when(val message = item.message) { - - // 첫 번째 매니저 채팅 - is ManagerFirstMessage -> { - when (message.type) { - - ManagerFirstMessageType.NORMAL -> { - // "전화왔어"가 포함된 경우 해당 단어만 브랜드 컬러로 강조 - val highlightWord = "전화왔어" - - val annotated = buildAnnotatedString { - // 강조할 키워드의 시작 위치 - val startIndex = message.content.indexOf(highlightWord) - - if (startIndex >= 0) { - // 키워드 앞부분은 기본 스타일로 추가 - append(message.content.substring(0, startIndex)) - - // 키워드에만 강조 색상을 적용 - withStyle(SpanStyle(color = MainVariant1)) { - append(highlightWord) - } - - // 키워드 이후 문자열을 이어서 추가 - append(message.content.substring(startIndex + highlightWord.length)) - } else { - // 키워드가 없는 경우 원본 문자열을 그대로 표시 - append(message.content) - } - } - ChatGrayBubble( - text = annotated, - 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 = AnnotatedString(message.content), - 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 = AnnotatedString(message.content), - time = item.time - ) - } - } - } - - is UserMessage -> { - Row( - modifier = Modifier.fillMaxWidth(), - 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 - ) - } - } + ManagerChatItemContent( + item = item + ) } } 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..0a31d4b --- /dev/null +++ b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/highlightText.kt @@ -0,0 +1,49 @@ +package kr.co.call.impl.component.chatroom.manager + +import androidx.compose.runtime.Composable +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/screen/ManagerChatRoomScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt index 1ab6c1e..a355502 100644 --- 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 @@ -75,7 +75,6 @@ fun ManagerChatRoomScreenContent( } } - Box( modifier = modifier .fillMaxSize() 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 index bc08d94..6f12df0 100644 --- 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 @@ -44,6 +44,7 @@ class ManagerChatRoomViewModel @Inject constructor( } } + // UI에 노출할 함수 fun handleIntent(intent: ManagerChatRoomIntent) { when (intent) { ManagerChatRoomIntent.ClickWhenCall -> @@ -57,15 +58,19 @@ class ManagerChatRoomViewModel @Inject constructor( } } + // 사용자의 선택 메시지를 추가한 뒤, 해당 요청에 대한 매니저 응답을 처리 private fun sendUserMessageThen(content: String, flow: Flow) = intent { val userMessage = UserMessage( id = UUID.randomUUID().toString(), // UI 상태에서 메시지를 구분하기 위한 임시 ID 생성 content = content, createdAt = LocalDateTime.now() ) + + // 유저 메세지 추가 reduce { state.copy(chatItems = state.chatItems + userMessage.toUiItem()) } + appendManagerMessages(flow) } From 2a3bd2f1bf4bbad0bbf2b996f99c9a8f9eb098f9 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 18:19:52 +0900 Subject: [PATCH 20/26] =?UTF-8?q?chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=9E=84=ED=8F=AC=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerChatItemContent.kt: SpanStyle, buildAnnotatedString, MainVariant1 등 미사용 텍스트 스타일 및 테마 관련 임포트 제거 - highlightText.kt: 미사용 Composable 어노테이션 임포트 제거 - ManagerChatLazyColumn.kt: Row, fillMaxWidth 등 레이아웃 관련 클래스 및 도메인 모델, 채팅 버블 컴포넌트 등 다수의 미사용 임포트 정리 --- .../chatroom/manager/ManagerChatItemContent.kt | 4 ---- .../chatroom/manager/ManagerChatLazyColumn.kt | 16 ---------------- .../component/chatroom/manager/highlightText.kt | 1 - 3 files changed, 21 deletions(-) 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 index c452583..e47f3e2 100644 --- 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 @@ -6,10 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable 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 kr.co.call.designsystem.theme.MainVariant1 import kr.co.call.domain.model.chatting.AskToAgentMessage import kr.co.call.domain.model.chatting.ChangePartnerInfoMessage import kr.co.call.domain.model.chatting.ChangePartnerInfoMessageType 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 index 2328130..dfce468 100644 --- 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 @@ -2,36 +2,20 @@ package kr.co.call.impl.component.chatroom.manager import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth 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.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 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp 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 @Composable 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 index 0a31d4b..58573e9 100644 --- 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 @@ -1,6 +1,5 @@ package kr.co.call.impl.component.chatroom.manager -import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString From 2012b781a1ba7c3e5945e28487ed945ac44a019b Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 18:38:09 +0900 Subject: [PATCH 21/26] =?UTF-8?q?refactor:=20ManagerChatRoomViewModel=20In?= =?UTF-8?q?tent=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Channel을 도입하여 사용자 Intent 처리를 직렬화하고 중복 요청 방지(capacity 1, DROP_LATEST 적용) - handleIntent 호출 시 이벤트를 직접 처리하지 않고 intentChannel로 전달하도록 변경 - ViewModel 초기화 시 intentChannel을 구독하여 전달된 Intent를 순차적으로 처리하는 로직 추가 - sendUserMessageThen 함수를 suspend 함수로 전환하여 유저 메시지 추가와 매니저 응답 Flow 처리의 순차성 보장 - processIntent 분리를 통해 Intent 처리 로직과 UI 액션 간의 매핑 구조 정리 --- .../viewmodel/ManagerChatRoomViewModel.kt | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) 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 index 6f12df0..72a7a20 100644 --- 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 @@ -2,6 +2,8 @@ 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 @@ -38,14 +40,36 @@ class ManagerChatRoomViewModel @Inject constructor( initialState = ManagerChatRoomUiState() ) + // 용량 1의 작업 큐. 가득 차면 새로 들어온 요청을 버림(DROP_LATEST) + private val intentChannel = Channel( + capacity = 1, + onBufferOverflow = BufferOverflow.DROP_LATEST + ) + init { intent { + // 첫 매니저 인사 appendManagerMessages(firstManagerChatUseCase()) + + // 채널에 쌓인 인텐트를 하나씩 순차 처리 (단일 코루틴이므로 직렬 처리가 보장) + 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()) @@ -59,18 +83,22 @@ class ManagerChatRoomViewModel @Inject constructor( } // 사용자의 선택 메시지를 추가한 뒤, 해당 요청에 대한 매니저 응답을 처리 - private fun sendUserMessageThen(content: String, flow: Flow) = intent { + private suspend fun Syntax.sendUserMessageThen( + content: String, + flow: Flow + ) { val userMessage = UserMessage( - id = UUID.randomUUID().toString(), // UI 상태에서 메시지를 구분하기 위한 임시 ID 생성 + id = UUID.randomUUID().toString(), content = content, createdAt = LocalDateTime.now() ) - // 유저 메세지 추가 + // 유저 메세지 추가. reduce { state.copy(chatItems = state.chatItems + userMessage.toUiItem()) } + // 매니저 메세지 flow 구독 appendManagerMessages(flow) } From b057d4c88eef92d44cfe944dd12cf239166f64cc Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 18:54:34 +0900 Subject: [PATCH 22/26] =?UTF-8?q?chore:=20=EC=B1=84=ED=8C=85=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=AC=B8=EC=84=9C=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=ED=94=84=EB=A6=AC=EB=B7=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerChatItemContent.kt: 각 메시지 타입별 처리 로직을 상세히 설명하는 KDoc 작성 및 다양한 메시지 유형을 확인할 수 있는 Preview 추가 - LoadingIndicator.kt: 점진적 애니메이션 구현을 위한 시작 지연 시간(delayMillis) 및 Y축 이동 로직에 대한 설명 주석 추가 - ManagerPromptSlide.kt: 드래그 제스처를 통한 슬라이드 패널의 확장/축소 상태 관리 로직 및 내부 UI 구성 요소에 대한 주석 추가 - ManagerChatRoomViewModel.kt: Intent 채널을 활용한 메시지 직렬 처리 보장 방식에 대한 설명 주석 보완 --- .../component/chatroom/LoadingIndicator.kt | 22 ++++- .../manager/ManagerChatItemContent.kt | 84 +++++++++++++++++-- .../chatroom/manager/ManagerPromptSlide.kt | 22 ++++- .../viewmodel/ManagerChatRoomViewModel.kt | 3 +- 4 files changed, 116 insertions(+), 15 deletions(-) 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 index 56f8d90..7ec39e0 100644 --- 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 @@ -48,10 +48,13 @@ fun LoadingIndicator( bounceHeight: Dp = 3.dp, ) { Row( - modifier = modifier.padding(top = bounceHeight), // 튀어오를 공간 확보 + // 점이 위로 이동할 때 잘리지 않도록 상단 여백 확보 + modifier = modifier.padding(top = bounceHeight), horizontalArrangement = Arrangement.spacedBy(spaceBetween), ) { repeat(3) { index -> + // 각 점마다 시작 지연 시간을 다르게 적용하여 + // 순차적으로 튀는 타이핑 인디케이터 효과 생성 AnimatedDot( color = dotColors[index], size = dotSize, @@ -78,17 +81,28 @@ private fun AnimatedDot( 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 - 1f at 300 using FastOutSlowInEasing // 300ms에 최고점 - 0f at 600 using FastOutSlowInEasing // 600ms에 원위치 + + // 300ms 후 최고점 도달 + 1f at 300 using FastOutSlowInEasing + + // 600ms 후 원위치 복귀 + 0f at 600 using FastOutSlowInEasing + // 600~900ms는 대기(idle) }, initialStartOffset = StartOffset(delayMillis), @@ -99,6 +113,8 @@ private fun AnimatedDot( Box( modifier = Modifier .size(size) + // progress 값에 따라 Y축 이동 적용 + // progress가 1일 때 가장 위로 이동하고 0일 때 원위치 .graphicsLayer { translationY = -progress * bounceHeight.toPx() } 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 index e47f3e2..d3b9df1 100644 --- 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 @@ -1,11 +1,17 @@ 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 @@ -21,17 +27,25 @@ import kr.co.call.impl.component.chatroom.ChatPinkBubble import kr.co.call.impl.model.ManagerChatUiItem /** - * 매니저와의 채팅 화면에서 각 메시지 타입에 따른 UI 콘텐츠를 렌더링하는 컴포저블 함수입니다. + * 매니저 채팅 화면에서 메시지 타입에 따른 UI 콘텐츠를 렌더링하는 컴포저블 함수입니다. * - * 이 함수는 [ManagerChatUiItem]의 데이터 모델을 기반으로 하며, 다음과 같은 메시지 유형을 처리합니다: - * - 매니저의 첫 인사 ([ManagerFirstMessage]): 특정 키워드 강조 및 타입별 버블 스타일 적용 - * - 상담원 문의 ([AskToAgentMessage]): 핑크색 배경의 말풍선 제공 - * - 상대방 정보 변경 및 기록 정보 수정 ([ChangePartnerInfoMessage], [UpdateInfoMessage]): 상태 변화에 따른 회색 또는 핑크색 버블 적용 - * - 유저 본인의 메시지 ([UserMessage]): 우측 정렬된 핑크색 버블 적용 - * - 전화 일정 안내 ([WhenCallMessage]): 회색 배경의 말풍선 제공 + * [ManagerChatUiItem]의 메시지 타입을 기반으로 적절한 채팅 버블을 표시하며, + * 메시지 종류에 따라 스타일 및 강조 처리를 적용합니다. * - * @param item 렌더링할 채팅 메시지 데이터와 상태 정보를 포함하는 UI 모델 객체 - * @param modifier 컴포저블의 레이아웃 및 동작을 수정하기 위한 [Modifier] + * 처리하는 메시지 유형: + * - [ManagerFirstMessage]: 매니저의 첫 안내 메시지 표시 + * - 일반 메시지는 특정 키워드를 브랜드 컬러로 강조 + * - 관계 형성 안내 메시지는 아이콘이 포함된 핑크색 버블 표시 + * - [AskToAgentMessage]: 상담원 문의 요청 메시지를 핑크색 버블로 표시 + * - [ChangePartnerInfoMessage]: 상대방 정보 변경 관련 메시지 표시 + * - 변경 완료 안내 메시지는 특정 문구를 브랜드 컬러로 강조 + * - [UpdateInfoMessage]: 기록 정보 수정 관련 메시지 표시 + * - 자연스러운 반영 안내 문구를 브랜드 컬러로 강조 + * - [UserMessage]: 사용자가 보낸 메시지를 우측 정렬된 핑크색 버블로 표시 + * - [WhenCallMessage]: 전화 일정 안내 메시지를 회색 버블로 표시 + * + * @param item 렌더링할 메시지 데이터와 로딩 상태를 포함하는 UI 모델 객체 + * @param modifier 컴포저블의 레이아웃 및 동작을 제어하기 위한 Modifier */ @Composable fun ManagerChatItemContent( @@ -143,4 +157,56 @@ fun ManagerChatItemContent( ) } } +} + +@Preview(showBackground = true) +@Composable +private fun ManagerChatItemContentPreview() { + val sampleItems = listOf( + ManagerChatUiItem( + message = ManagerFirstMessage( + id = "1", + content = "안녕하세요! 전화왔어 매니저입니다.", + type = ManagerFirstMessageType.NORMAL, + createdAt = LocalDateTime.now() + ), + time = "오후 2:00" + ), + ManagerChatUiItem( + message = ManagerFirstMessage( + id = "2", + content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", + type = ManagerFirstMessageType.RELATIONSHIP, + createdAt = LocalDateTime.now() + ), + time = "오후 2:01" + ), + ManagerChatUiItem( + message = UserMessage( + id = "3", + content = "반가워요!", + createdAt = LocalDateTime.now() + ), + time = "오후 2:02" + ), + ManagerChatUiItem( + 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/ManagerPromptSlide.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/component/chatroom/manager/ManagerPromptSlide.kt index e447786..9f05ca9 100644 --- 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 @@ -37,11 +37,22 @@ 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( @@ -54,6 +65,9 @@ fun ManagerPromptSlide( .background(Color.White, RoundedCornerShape(30.dp)) .draggable( orientation = Orientation.Vertical, + // 세로 드래그 방향에 따라 패널 확장/축소 상태 변경 + // 위로 일정 거리 이상 드래그하면 확장, + // 아래로 일정 거리 이상 드래그하면 축소 state = rememberDraggableState { delta -> if (delta < -5f) isExpanded = true if (delta > 5f) isExpanded = false @@ -63,7 +77,7 @@ fun ManagerPromptSlide( horizontalAlignment = Alignment.CenterHorizontally ) { - // 드래그 핸들 + // 사용자에게 드래그 가능한 영역임을 나타내는 핸들 표시 Box( modifier = Modifier .size(width = 62.dp, height = 4.dp) @@ -101,15 +115,19 @@ private fun PromptItem( modifier = Modifier .fillMaxWidth() .height(40.dp) - .clickable(onClick = onClick) + .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( 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 index 72a7a20..d7deaa3 100644 --- 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 @@ -51,7 +51,8 @@ class ManagerChatRoomViewModel @Inject constructor( // 첫 매니저 인사 appendManagerMessages(firstManagerChatUseCase()) - // 채널에 쌓인 인텐트를 하나씩 순차 처리 (단일 코루틴이므로 직렬 처리가 보장) + // 채널에 전달된 Intent를 지속적으로 소비하며 순차 처리. + // 단일 코루틴이므로 직렬 처리 보장 for (chatIntent in intentChannel) { processIntent(chatIntent) } From 7dea1440b8fac57138f96fe69f666e8c844ce2b9 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 19:17:23 +0900 Subject: [PATCH 23/26] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=82=A0=EC=A7=9C=20=EA=B5=AC=EB=B6=84=EC=84=A0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerChatUiItem을 sealed class로 개편하여 Message와 DateSeparator 타입으로 분리 - ManagerChatRoomViewModel에 마지막 메시지와 날짜가 다를 경우 DateSeparator를 삽입하는 로직 추가 - ManagerChatLazyColumn에서 아이템 타입에 따라 날짜 구분선과 메시지 컨텐츠를 구분하여 렌더링 - LazyColumn의 items key 생성 로직에 DateSeparator 전용 식별자 추가 - UiModelMapper 및 ManagerChatItemContent가 변경된 Message 모델 구조를 사용하도록 수정 - Preview 및 테스트용 더미 데이터에 날짜 구분선 관련 코드 반영 --- .../manager/ManagerChatItemContent.kt | 10 ++--- .../chatroom/manager/ManagerChatLazyColumn.kt | 29 ++++++++++---- .../kr/co/call/impl/mapper/UiModelMapper.kt | 4 +- .../co/call/impl/model/ManagerChatUiItem.kt | 16 +++++--- .../call/impl/screen/ManagerChatRoomScreen.kt | 29 ++++++++------ .../viewmodel/ManagerChatRoomViewModel.kt | 38 +++++++++++++++++-- 6 files changed, 92 insertions(+), 34 deletions(-) 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 index d3b9df1..4979aa6 100644 --- 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 @@ -49,7 +49,7 @@ import kr.co.call.impl.model.ManagerChatUiItem */ @Composable fun ManagerChatItemContent( - item: ManagerChatUiItem, + item: ManagerChatUiItem.Message, modifier: Modifier = Modifier, ) { when (val message = item.message) { @@ -163,7 +163,7 @@ fun ManagerChatItemContent( @Composable private fun ManagerChatItemContentPreview() { val sampleItems = listOf( - ManagerChatUiItem( + ManagerChatUiItem.Message( message = ManagerFirstMessage( id = "1", content = "안녕하세요! 전화왔어 매니저입니다.", @@ -172,7 +172,7 @@ private fun ManagerChatItemContentPreview() { ), time = "오후 2:00" ), - ManagerChatUiItem( + ManagerChatUiItem.Message( message = ManagerFirstMessage( id = "2", content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", @@ -181,7 +181,7 @@ private fun ManagerChatItemContentPreview() { ), time = "오후 2:01" ), - ManagerChatUiItem( + ManagerChatUiItem.Message( message = UserMessage( id = "3", content = "반가워요!", @@ -189,7 +189,7 @@ private fun ManagerChatItemContentPreview() { ), time = "오후 2:02" ), - ManagerChatUiItem( + ManagerChatUiItem.Message( message = WhenCallMessage( id = "4", content = "오늘 오후 3시에 전화 드릴게요.", 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 index dfce468..eae0c95 100644 --- 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 @@ -11,13 +11,18 @@ 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, @@ -37,13 +42,20 @@ fun ManagerChatLazyColumn( ) { items( items = chatItems, - key = { it.message.id } + key = { item -> + when (item) { + is ManagerChatUiItem.Message -> item.message.id + is ManagerChatUiItem.DateSeparator -> "date-${item.date}" + } + } ) { item -> - ManagerChatItemContent( - item = item - ) + when (item) { + is ManagerChatUiItem.Message -> ManagerChatItemContent(item = item) + is ManagerChatUiItem.DateSeparator -> DateSeparator( + text = item.date.format(dateSeparatorFormatter) + ) + } } - } } @@ -51,7 +63,8 @@ fun ManagerChatLazyColumn( @Composable private fun ManagerChatLazyColumnPreview() { val sampleChatItems = listOf( - ManagerChatUiItem( + ManagerChatUiItem.DateSeparator(date = LocalDate.now()), + ManagerChatUiItem.Message( message = ManagerFirstMessage( id = "1", content = "안녕하세요! 무엇을 도와드릴까요?", @@ -60,7 +73,7 @@ private fun ManagerChatLazyColumnPreview() { ), time = "오후 2:00" ), - ManagerChatUiItem( + ManagerChatUiItem.Message( message = ManagerFirstMessage( id = "2", content = "대화를 많이 나눌수록,\n더 자연스러운 관계가 생성돼요.", @@ -69,7 +82,7 @@ private fun ManagerChatLazyColumnPreview() { ), time = "오후 2:01" ), - ManagerChatUiItem( + ManagerChatUiItem.Message( message = ManagerFirstMessage( id = "3", content = "로딩 중...", 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 797c95b..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 @@ -18,8 +18,8 @@ object UiModelMapper { fun ManagerChatItem.toUiItem( loadStatus: LoadStatus = LoadStatus.Idle, - ): ManagerChatUiItem = - ManagerChatUiItem( + ): ManagerChatUiItem.Message = + ManagerChatUiItem.Message( message = this, loadStatus = loadStatus, time = this.createdAt.format(timeFormatter), 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 index 4932269..d521c55 100644 --- 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 @@ -1,10 +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 -data class ManagerChatUiItem( - val message: ManagerChatItem, - val loadStatus: LoadStatus = LoadStatus.Idle, - val time: String = "", -) \ No newline at end of file +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/ManagerChatRoomScreen.kt b/feature/chatting/impl/src/main/java/kr/co/call/impl/screen/ManagerChatRoomScreen.kt index a355502..20e53ea 100644 --- 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 @@ -24,6 +24,7 @@ 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 @@ -132,7 +133,8 @@ private fun ManagerChatRoomScreenContentPreview() { ManagerChatRoomScreenContent( state = ManagerChatRoomUiState( chatItems = listOf( - ManagerChatUiItem( + ManagerChatUiItem.DateSeparator(date = LocalDate.now()), + ManagerChatUiItem.Message( message = ManagerFirstMessage( id = "1", content = "안녕하세요! 무엇을 도와드릴까요?", @@ -155,16 +157,21 @@ private fun ManagerChatRoomScreenContentTestPreview() { CallFromAiTheme { ManagerChatRoomScreenContent( state = ManagerChatRoomUiState( - chatItems = List(40) { i -> - ManagerChatUiItem( - message = ManagerFirstMessage( - id = "$i", // 유니크해야 함 - content = "메시지 $i - 스크롤 테스트용 더미 데이터입니다.", - type = ManagerFirstMessageType.NORMAL, - createdAt = LocalDateTime.now() - ), - time = "오전 10:${(i % 60).toString().padStart(2, '0')}" - ) + 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 = {}, 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 index d7deaa3..9e56810 100644 --- 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 @@ -8,6 +8,7 @@ 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 @@ -18,6 +19,7 @@ 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 @@ -96,7 +98,11 @@ class ManagerChatRoomViewModel @Inject constructor( // 유저 메세지 추가. reduce { - state.copy(chatItems = state.chatItems + userMessage.toUiItem()) + state.copy( + chatItems = state.chatItems + + dateSeparatorIfNeeded(state.chatItems, userMessage.createdAt.toLocalDate()) + + userMessage.toUiItem() + ) } // 매니저 메세지 flow 구독 @@ -112,13 +118,18 @@ class ManagerChatRoomViewModel @Inject constructor( ) { flow.collect { message -> reduce { - state.copy(chatItems = state.chatItems + message.toUiItem(loadStatus = LoadStatus.Loading)) + 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.message.id == message.id) item.copy(loadStatus = LoadStatus.Idle) + if (item is ManagerChatUiItem.Message && item.message.id == message.id) + item.copy(loadStatus = LoadStatus.Idle) else item } ) @@ -126,4 +137,25 @@ class ManagerChatRoomViewModel @Inject constructor( } } + // 마지막 메시지의 날짜와 다를 경우에만 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 From d1dcd3573cef015550b2551053040e89d0f89ca8 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 19:22:15 +0900 Subject: [PATCH 24/26] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=8C=A8=EB=94=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ManagerChatItemContent.kt: UserMessage 레이아웃의 Row에 상하 18.dp 패딩 적용 - ManagerChatItemContent.kt: 사용자 메시지 우측 정렬 방식에 대한 설명 주석 추가 및 Modifier 포맷팅 수정 - ManagerChatRoomViewModel.kt: delay 함수 호출부 전후에 공백 라인을 추가하여 코드 가독성 향상 --- .../component/chatroom/manager/ManagerChatItemContent.kt | 5 ++++- .../kr/co/call/impl/viewmodel/ManagerChatRoomViewModel.kt | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) 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 index 4979aa6..8af74aa 100644 --- 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 @@ -136,9 +136,12 @@ fun ManagerChatItemContent( } // 유저 메세지 + // 오른쪽 정렬을 위해 Arrangement.End인 Row 사용 is UserMessage -> { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 18.dp), horizontalArrangement = Arrangement.End ) { ChatPinkBubble( 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 index 9e56810..c8768d1 100644 --- 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 @@ -124,7 +124,9 @@ class ManagerChatRoomViewModel @Inject constructor( + message.toUiItem(loadStatus = LoadStatus.Loading) ) } + delay(1500.milliseconds) + reduce { state.copy( chatItems = state.chatItems.map { item -> From c74ffa75d84592ad3c5043420568d0d8ab30fed2 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 19:56:07 +0900 Subject: [PATCH 25/26] =?UTF-8?q?refactor:=20=EB=A7=A4=EB=8B=88=EC=A0=80?= =?UTF-8?q?=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=ED=81=B4=EB=A6=AD=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatListViewModel: ChatListIntent.ClickManagerChatRoom 발생 시 TODO를 제거하고 emitNavigateToManagerChatRoom() 호출하도록 수정 - ChatListScreen: Screen 및 ScreenContent 컴포넌트에서 불필요한 onChatRoomClick, onManagerChatRoomClick 콜백 파라미터 제거 - ChatListScreenContent: 매니저 채팅방 아이템 클릭 시 onIntent를 통해 ClickManagerChatRoom 인텐트를 전달하도록 변경 - ChatListScreen_Preview: 변경된 파라미터 구조에 맞춰 프리뷰 코드 수정 --- .../main/java/kr/co/call/impl/screen/ChatListScreen.kt | 8 +------- .../java/kr/co/call/impl/viewmodel/ChatListViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) 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 3f9a172..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 @@ -49,8 +49,6 @@ fun ChatListScreen( ChatListScreenContent( state = state, - onChatRoomClick = onChatRoomClick, - onManagerChatRoomClick = onManagerChatRoomClick, onIntent = viewModel::handleIntent, modifier = modifier ) @@ -59,8 +57,6 @@ fun ChatListScreen( @Composable fun ChatListScreenContent( state: ChatListState, - onChatRoomClick: (Long) -> Unit, - onManagerChatRoomClick: () -> Unit, onIntent: (ChatListIntent) -> Unit, modifier: Modifier = Modifier, ) { @@ -119,7 +115,7 @@ fun ChatListScreenContent( name = "전화왔어 매니저", content = "수현님, 반가워요! 👋🏻 오늘은 어떤 이야기를", ), - onClick = onManagerChatRoomClick + onClick = { onIntent(ChatListIntent.ClickManagerChatRoom) } ) } } @@ -156,8 +152,6 @@ private fun ChatListScreenContentPreview() { ), status = LoadStatus.Idle ), - onChatRoomClick = { _ -> }, - onManagerChatRoomClick = {}, onIntent = {} ) } 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 de87fff..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 @@ -51,7 +51,7 @@ class ChatListViewModel @Inject constructor( fun handleIntent(intent: ChatListIntent) { when (intent) { is ChatListIntent.ClickChatRoom -> emitNavigateToChatRoom(intent.roomId) - ChatListIntent.ClickManagerChatRoom -> TODO() + ChatListIntent.ClickManagerChatRoom -> emitNavigateToManagerChatRoom() } } From f09a30986d59e5ca53ab9bb21b91dbe8d654f888 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Mon, 20 Jul 2026 20:00:57 +0900 Subject: [PATCH 26/26] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EB=A7=90?= =?UTF-8?q?=ED=92=8D=EC=84=A0=20=EC=B5=9C=EB=8C=80=20=EB=84=88=EB=B9=84=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatPinkBubble.kt: Text 컴포넌트에 `widthIn(max = 280.dp)` Modifier를 추가하여 말풍선의 최대 너비 제한 --- .../java/kr/co/call/impl/component/chatroom/ChatPinkBubble.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index bcf3a95..46dc4f2 100644 --- 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 @@ -5,6 +5,7 @@ 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 @@ -32,6 +33,7 @@ fun ChatPinkBubble( Text( text = text, modifier = Modifier + .widthIn(max = 280.dp) .background( brush = CallTheme.colors.chatGradientReverse, shape = RoundedCornerShape(