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