이 문서에서는 Android 애플리케이션에서 헤드리스 모바일 SDK를 통합하고 맞춤설정하는 방법을 보여줍니다. 여기에서는 현지화, 세션 후 흐름, 기타 고급 주제를 다룹니다.
세션 후 흐름 (세션 후 VA, 설문조사, 고객만족도)
실시간 채팅이나 음성 통화가 종료되면 고객센터에서는 의견을 수집하거나 자동 후속 조치를 제공하는 경우가 많습니다. CCAI Platform은 세 가지 유형의 세션 후 경험을 지원합니다.
세션 후 가상 에이전트 (VA): 인간 상담사의 연결이 해제된 후 대화를 계속하는 자동 챗봇입니다.
설문조사: 특정 대기열에 맞게 조정된 다중 질문 양식입니다.
CSAT (고객 만족도): 평점 (예: 1~5개 별) 및 선택적 텍스트 의견입니다.
계층 구조 적용
관리자가 포털에서 중복되는 규칙을 구성하는 것이 일반적입니다 (예: CSAT를 전역적으로 사용 설정하지만 결제 대기열에 대해 세션 후 VA도 사용 설정).
SDK에서 제공하는 기능: 엄격한 충돌 해결 엔진 SDK는 포털 규칙을 평가하고 세션당 하나의 세션 후 흐름만 트리거되도록 다음 계층 구조를 엄격하게 준수합니다.
세션 후 우선순위: 세션 후 VA > 설문조사 > CSAT
- 예: 대기열에 CSAT와 세션 후 VA가 모두 사용 설정된 경우 사용자에게는 세션 후 VA만 표시됩니다.
빌드하는 항목: 세션이 종료되기를 기다리고, SDK에 계층 구조 평가에서 승리한 흐름을 묻고, 해당 화면을 그리는 리스너 어떤 환경이 승리하는지 파악하기 위해 복잡한 if-else 로직을 작성할 필요가 없습니다. SDK가 이를 계산해 줍니다.
구현 예시
이 스니펫은 앱이 세션 종료를 수신 대기하고 SDK에 다음 작업을 묻는 방법을 보여줍니다.
// Called when the active chat or call ends
fun handleSessionEnded(postSessionFlow: PostSessionFlow?) {
// 1. Check if there is a required post-session flow.
// The SDK has already calculated the hierarchy (VA > Survey > CSAT).
val flow = postSessionFlow
if (flow == null) {
// No feedback required. Safely close the support UI.
closeSupportExperience()
return
}
// 2. Route to the winning flow returned by the SDK.
when (flow.type) {
PostSessionType.VIRTUAL_AGENT -> transitionToPostSessionVA()
PostSessionType.SURVEY -> showSurveyScreen(flow)
PostSessionType.CSAT -> showCSATScreen(flow)
}
}
세션 후 단계로 전환
계층 구조에서 어떤 흐름이 우선하든 chatService에서 두 개의 공유 수명 주기 메서드를 호출하여 세션 후 단계가 시작되었음을 CCAI Platform 플랫폼에 알려야 합니다.
SDK 제공 항목: 세션을 세션 후 단계로 전환하는 수명 주기 메서드 (startPostSession, readyPostSession)
빌드하는 항목: 세션 후 UI 흐름의 올바른 지점에서 이러한 메서드를 호출하는 오케스트레이션 로직
구현 예시
이 코드는 세션을 두 개의 세션 후 수명 주기 단계로 전환합니다.
lifecycleScope.launch {
// Step 1: Start the post-session process (updates status to "in_progress")
try {
val postSessionResponse = CCAI.chatService?.startPostSession()
if (postSessionResponse != null) {
Log.d("CCAI", "Post-session started successfully: ${postSessionResponse.id}")
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to start post-session: ${e.message}")
}
// Step 2: Signal that post-session is ready (updates status to "ready")
try {
val readyResponse = CCAI.chatService?.readyPostSession()
if (readyResponse != null) {
Log.d("CCAI", "Post-session ready: ${readyResponse.id}")
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to prepare post-session: ${e.message}")
}
}
플랫폼에서 전환을 확인한 후 이 특정 흐름으로 진행합니다.
ChatResponse의 세션 후 상태 필드
ChatResponse의 두 세션 후 조정 필드는 전환 로직을 다음과 같이 제어합니다.
postSessionTransferStatus: String?- 세션 후 전송의 현재 상태입니다 (예:"in_progress","ready","completed"). 플랫폼은 이 필드를 기반으로 오케스트레이션 상태 머신을 키로 지정합니다.startPostSession()는"in_progress"로 이동하고readyPostSession()는"ready"로 이동하며 후속ChatResponse배출 (채팅 서비스 채팅 흐름에서)에서 이 값을 관찰하여 플랫폼 측 전환을 확인합니다.postSessionOptInRequired: Boolean?- 세션 후 흐름이 실행되기 전에 사용자가 명시적으로 선택해야 하는지 여부입니다.true인 경우 UI에 선택 메시지를 표시하고 사용자가 동의한 후에만startPostSession()를 호출합니다.false또는null인 경우 선택 확인이 필요하지 않으며 세션 후 흐름으로 바로 전환할 수 있습니다.
suspend fun orchestratePostSession(chat: ChatResponse) {
// 1. Honor opt-in gating if required
if (chat.postSessionOptInRequired == true) {
val consented = presentPostSessionOptIn()
if (!consented) {
closeSupportExperience()
return
}
}
// 2. Drive the state machine
CCAI.chatService?.startPostSession() // → "in_progress"
CCAI.chatService?.readyPostSession() // → "ready"
// 3. Observe postSessionTransferStatus on subsequent chat updates
// to confirm the transfer completed before rendering the winning flow.
}
postSessionRequired는 세션에 하나라도 세션 후 흐름이 구성되어 있는지 알려줍니다.postSessionOptInRequired는 실행하기 전에 명시적 사용자 동의가 필요한지 여부를 알려줍니다.postSessionTransferStatus는 전환 수명 주기에서 플랫폼의 위치를 알려줍니다.
세 가지 모두 독립적이므로 항상 각각 확인하세요.
세션 후 가상 에이전트 (최고 우선순위)
SDK에서 제공하는 기능: 세션을 인간 상담사에서 자동 봇으로 다시 전환하는 전환 상태와 봇 메시지의 라우팅
빌드할 항목: 완전히 새로운 항목은 없습니다. 세션 후 VA는 채팅으로 작동하므로 기존 맞춤 채팅 UI를 열어 두고 인간 상담원의 메시지를 처리한 것처럼 수신되는 자동 메시지를 처리하도록 합니다.
구현 예시
SDK가 세션 후 VA로의 전환을 알리면 변경사항을 반영하도록 채팅 UI를 업데이트합니다.
fun transitionToPostSessionVA() {
// Keep your chat UI open, but update the visual context
myChatAdapter.addSystemMessage(
"You are now speaking with our Virtual Assistant"
)
agentNameTextView.text = "Virtual Assistant"
agentAvatarImageView.setImageResource(R.drawable.ic_bot_avatar)
// The SDK will automatically start piping the VA's messages into
// your standard chat message observer (messagesReceived).
}
설문조사 (중간 우선순위)
SDK 제공 사항: rateService는 설문조사 질문을 가져오고 커뮤니케이션 세션의 답변을 제출하는 메서드를 노출합니다. 설문조사 질문은 각 대기열에 대해 관리자가 구성하며 여러 질문 유형 (CSAT, 별점, 자유 형식 텍스트, 열거, 숫자 척도)을 포함할 수 있습니다.
빌드할 항목: 설문조사를 가져오고, 질문을 통해 루프를 실행하고, 각 질문에 적절한 UI (예: 라디오 버튼, 별표 평점, 텍스트 필드)를 렌더링하고, 필수 필드가 작성되었는지 확인하고, 다시 전송할 답변을 패키징하는 동적 양식
키 유형
// Survey response from getSurvey()
data class Survey(
val id: Int?,
val lang: String?,
val questions: List<SurveyQuestion>?,
val signOffText: String? // "Thank you" message to show after submission
)
// Request payload for sendSurvey()
data class SurveyRequest(
val answers: Map<Int, String> // Maps question IDs to answer values
)
구현 예시
이 코드는 rateService에서 설문조사 질문을 가져와 맞춤 프래그먼트에 렌더링하고 답변을 제출합니다.
fun showSurveyScreen(communicationId: Long) {
val rateService = CCAI.rateService ?: return
lifecycleScope.launch {
try {
// 1. Fetch the survey questions for this session
val survey = rateService.getSurvey(
communicationId = communicationId,
communicationType = "chats"
).getOrThrow()
// 2. Present your custom survey UI
val surveyFragment = MyCustomSurveyFragment.newInstance(
survey.questions.orEmpty()
)
surveyFragment.onSubmit = { answersMap ->
lifecycleScope.launch {
try {
// 3. Submit the answers
val request = SurveyRequest(answers = answersMap)
rateService.sendSurvey(
communicationId = communicationId,
communicationType = "chats",
answers = request
).getOrThrow()
rateService.clear()
closeSupportExperience()
} catch (e: Exception) {
Log.e("CCAI", "Failed to submit survey: ${e.message}")
}
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.container, surveyFragment)
.addToBackStack(null)
.commit()
} catch (e: Exception) {
Log.e("CCAI", "Failed to load survey: ${e.message}")
closeSupportExperience()
}
}
}
CSAT - 고객 만족도 (가장 낮은 우선순위)
SDK에서 제공하는 기능: rateService는 커뮤니케이션 세션의 CSAT 점수와 선택적 의견을 제출하는 rate() 메서드를 노출합니다. 평점은 일반적으로 1~5의 정수 값입니다.
빌드하는 항목: 시각적 평가 화면 (예: 탭할 수 있는 별표 행 또는 1~5 숫자 척도), 의견이 허용되는 경우 텍스트 필드, 제출 버튼
키 유형
// Request payload for rate()
data class RatingRequest(
val rating: Int? = null, // Typically 1-5
val feedback: String? = null // Optional qualitative feedback
)
구현 예시
사용자가 별표 평점을 탭하고 의견을 작성하면 이러한 값을 rateService에 전달합니다.
fun submitCSATFeedback(communicationId: Long, rating: Int, comment: String?) {
val rateService = CCAI.rateService ?: return
submitButton.isEnabled = false
progressBar.isVisible = true
lifecycleScope.launch {
try {
val request = RatingRequest(
rating = rating,
feedback = comment?.trim()?.takeIf { it.isNotEmpty() }
)
rateService.rate(
communicationId = communicationId,
communicationType = "chats",
request = request
).getOrThrow()
rateService.clear()
showThankYouScreen()
} catch (e: Exception) {
showErrorToast("Failed to submit feedback. Please try again.")
} finally {
submitButton.isEnabled = true
progressBar.isVisible = false
}
}
}
채팅 스크립트 다운로드
SDK에서 제공하는 기능: 채팅 스크립트를 PDF 파일로 다운로드하는 메서드
빌드하는 항목: UI의 다운로드 버튼과 파일 처리 로직 (저장, 공유 또는 미리보기)
구현 예시
이 코드는 Android 스크립트 다운로드 접근 방식을 보여줍니다. PDF를 파일로 가져온 다음 앱에서 저장, 공유 또는 미리보기합니다.
// Download to a file destination
val documentsDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
val transcriptFile = File(documentsDir, "chat_transcript.pdf")
lifecycleScope.launch {
try {
val transcriptFile = CCAI.chatService?.downloadChatTranscript()
if (transcriptFile != null) {
Log.d("CCAI", "Transcript downloaded to: ${transcriptFile.absolutePath}")
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to download transcript: ${e.message}")
}
}
Android downloadChatTranscript() 메서드는 매개변수를 사용하지 않고 다운로드된 PDF를 가리키는 File?를 반환합니다. downloadChatTranscriptData() 메서드 (디스크에 저장하지 않고 메모리에서 직접 원시 PDF 데이터를 반환)는 iOS에서만 사용할 수 있습니다. 공유 저장소에 저장하는 경우 앱에 적절한 파일 저장소 권한이 있는지 확인합니다.
언어 및 현지화
헤드리스 모바일 SDK를 사용하여 맞춤 내장 UI를 빌드하므로 앱의 버튼, 메뉴, 맞춤 알림의 번역은 사용자가 소유합니다.
그렇다면 SDK가 사용자의 언어를 알아야 하는 이유는 무엇일까요?
대기열 라우팅: CCAI Platform은 언어 코드를 사용하여 해당 언어를 사용하는 상담사에게 사용자를 라우팅합니다.
시스템 메시지: CCAI Platform 백엔드에서 생성된 자동 메시지 (예: '상담사가 곧 연결됩니다')는 활성 언어로 제공됩니다.
언어 서비스 API
SDK에서 제공하는 항목: 활성 SDK 언어의 읽기 및 쓰기 languageCode 속성
빌드하는 항목: 언어 선택 도구 UI (사용자가 직접 선택하도록 하려는 경우) 및 세션이 시작되기 전에 언어를 설정하는 로직
// Read the active language
val currentLanguage = CCAI.languageService?.languageCode
// Set the active language before the user starts a support session
CCAI.languageService?.languageCode = "es"
SDK는 고정된 SDK 지원 언어 목록을 노출하지 않습니다. 앱에 언어 선택 도구가 표시되는 경우 앱의 표시 목록을 직접 유지하거나 SDK에서 반환된 인스턴스 또는 엔터프라이즈 구성을 사용하여 제공할 언어를 결정합니다.
SDK 언어 설정 (세션 전 규칙)
SDK에서 제공하는 항목: 요청한 언어를 관리자가 CCAI Platform 포털에서 실제로 사용 설정한 언어와 비교하는 LanguageService입니다.
빌드하는 항목: 사용자가 기기의 기본 설정에만 의존하지 않고 언어를 수동으로 재정의하도록 허용하려면 선택 UI(예: 드롭다운 메뉴)를 빌드하고 사용자가 지원 채널을 탭하기 전에만 액세스할 수 있는지 확인해야 합니다.
구현 예시
이 코드는 표준 ISO 639 언어 코드 (예: 스페인어는 "es", 프랑스어는 "fr")를 사용하여 SDK의 언어를 명시적으로 재정의하는 방법을 보여줍니다. 기본 언어는 InitOptions 데이터 클래스에서 SDK를 초기화하는 동안 languageCode 매개변수를 사용하여 설정됩니다. SDK를 다시 초기화할 때도 설정할 수 있습니다.
// Set language during initialization
val initOptions = InitOptions(
key = "YOUR_COMPANY_KEY",
urlHost = "your_subdomain.ccaiplatform.com",
languageCode = "es" // for example, "es" for Spanish, "fr" for French
)
try {
CCAI.initialize(context = this, options = initOptions)
} catch (e: Exception) {
Log.e("CCAI", "Failed to initialize CCAI: ${e.message}")
}
채팅 모듈 초기화 (Android)
언어 코드를 허용하는 오버로드를 사용하여 채팅 모듈을 초기화하는 경우 언어를 여기에 제공할 수도 있습니다.
val chatOptions = ChatOptions(
webFormInterface = WebFormManager(),
downloadTranscriptVisibility = DownloadTranscriptVisibility.SHOW_ALL,
greeting = "Hello! How can I help you?"
)
CCAI.initializeChat(context = this, options = chatOptions)
대기열 메뉴를 가져올 때 언어를 전달할 수도 있습니다.
val menuResult = CCAI.queueMenuService?.get(key = null)
ChatRequest를 사용하여 세션별로 재정의합니다.
val chatRequest = ChatRequest(
chat = Chat(
menuId = menuId,
languageCode = "fr" // This session will use French
)
)
CCAI Platform에서 최종 언어를 확인하는 방법
언어를 명시적으로 설정하지 않더라도 SDK에는 라우팅 및 시스템 메시지에 사용할 언어 코드가 필요합니다. 예측 가능한 동작을 유지하려면 사용자가 지원 세션을 시작하기 전에 직접 설정하는 것이 좋습니다.
다음 우선순위를 사용하세요.
명시적 언어 (권장):
InitOptions.languageCode에서 언어를 설정하면 SDK가 해당 언어를 사용합니다.세션 전 언어 업데이트 (여전히 명시적): 사용자가 대기열에 들어가기 전에
CCAI.languageService?.languageCode = "es"를 설정하면 SDK에서 업데이트된 언어를 사용합니다.세션별 재정의 (채팅 전용):
ChatRequest(chat = Chat(..., languageCode = "fr"))를 제공하면 해당 채팅 세션에서 요청된 언어 코드를 사용합니다.아무것도 설정하지 않는 경우: 동작은 SDK에서 기기 및 인스턴스 구성에서 가장 적합한 옵션으로 판단하는 것으로 대체됩니다. 일관된 결과를 얻으려면 이 기능을 사용하지 말고 항상 언어를 명시적으로 설정하세요 (1~3단계).
오류 처리, 대체, 세션 관리
가장 강력한 앱조차 연결이 끊어지거나, 종속 항목이 누락되거나, 인증 문제가 발생할 수 있습니다. 헤드리스 모바일 SDK는 이러한 기본 플랫폼 문제를 포착하여 앱에 표시하도록 설계되어 있으므로 맞춤 UI 내에서 원활하게 처리하거나 CCAI 플랫폼의 자동 대체 기능을 사용할 수 있습니다.
SDK 오류 처리
SDK에서 제공하는 항목: 표준 Kotlin 예외와 함께 두 가지 유형의 예외
HttpExceptionWithCode- CCAI 플랫폼에 대한 HTTP 요청이 실패할 때 비동기 서비스 메서드에 의해 발생합니다. 상태 코드 (예: 만료된 인증의 경우401, 비율 제한의 경우429, 서버 오류의 경우5xx)에 따라 분기할 수 있도록Int code속성을 노출합니다.CallCreationError.AfterHours- 슬롯 충돌이 발생하거나 일정 예약 기간이 닫힐 때 예약된 호출 API (callService.createScheduledCall)에서 발생한 봉인된 오류입니다. 사용자에게 직접 표시하기 위한 서버 현지화된displayMessage를 전달합니다.표준 Kotlin 또는 Java 예외 - 네트워크 오류는
java.io.IOException로, 코루틴 취소는kotlinx.coroutines.CancellationException로, 예상치 못한 항목은 기본Exception클래스로 표시됩니다.
빌드하는 항목: 모든 SDK 서비스 호출 주변에 계층화된 try 또는 catch 가장 구체적인 예외 (해당하는 경우 CallCreationError.AfterHours와 같은 도메인별 봉인 오류, HTTP 상태 분기의 경우 HttpExceptionWithCode)를 먼저 포착하고 네트워크, 시스템, 예상치 못한 오류의 경우 일반적인 Exception 핸들러로 대체합니다.
구현 예시
이 코드는 SDK 호출을 계층화된 try 또는 catch 패턴으로 래핑하여 먼저 HttpExceptionWithCode를 포착하여 HTTP 상태 코드로 분기한 다음 다른 모든 항목에 대한 일반 핸들러로 대체합니다.
import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope
lifecycleScope.launch {
try {
CCAI.chatService?.start(chatRequest)
// Handle success - observe chat state using chatReceived
} catch (e: HttpExceptionWithCode) {
// Handle HTTP-level failures from CCAI endpoints - inspect e.code for status code
Log.e("CCAI", "HTTP ${e.code}: ${e.message}")
showErrorToast("Something went wrong. Please try again.")
} catch (e: Exception) {
// Handle other errors (examples: network and system)
Log.e("CCAI", "Error: ${e.message}")
showErrorToast("An unexpected error occurred.")
}
}
권장사항
모든 SDK 서비스 호출을 계층화된 try/catch로 래핑합니다.
HttpExceptionWithCode를 사용하여 HTTP 상태 코드를 기반으로 분기합니다. 예를 들어401(만료된 인증 토큰)에서ccaiShouldAuthenticate()을 다시 트리거하거나,429(속도 제한)에 백오프를 적용하거나,5xx응답에 일반 오류를 표시합니다.예약된 호출 API의 경우 409 슬롯 충돌이 서버 현지화된 메시지로 처리되도록
HttpExceptionWithCode전에CallCreationError.AfterHours를 포착합니다.마지막으로
IOException,CancellationException및 예기치 않은 오류에 대한 안전망으로 일반Exception를 포착합니다. 개발 중에 예외 세부정보를 로깅하면 근본 원인을 신속하게 파악할 수 있습니다.
세션 확인 및 삭제
앱의 수명 주기는 CCAI Platform 지원 수명 주기와 별개이므로 세션 상태(특히 사용자 로그아웃 관련)를 관리해야 합니다.
SDK에서 제공하는 기능: 지원 세션이 아직 진행 중인지 확인하는 유틸리티 API (앱 다시 시작 또는 강제 종료 후 포함) 및 특정 최종 사용자와 연결된 캐시된 SDK 인증 상태를 지우는 메서드 - 다음 로그인 사용자가 이전 사용자의 세션을 상속하지 못하도록 방지합니다.
빌드하는 항목: 새 세션을 시작하기 전 사전 비행 점검과 사용자가 기본 앱에서 로그아웃할 때 실행되는 정리 루틴
구현 예 (로그아웃 정리)
활성 세션을 종료하고, SDK에서 캐시된 인증 토큰을 지우고, 로컬 앱 데이터를 삭제하고, 로그인 화면으로 이동합니다. 이는 사용자가 로그아웃할 때 세션 상속을 방지하는 데 필요한 최소 정리입니다.
fun handleUserLogout() {
lifecycleScope.launch {
// 1. Check for an in-progress session and end it cleanly
try {
val activeChat = CCAI.chatService?.getLastChatInProgress()
if (activeChat != null) {
CCAI.chatService?.endChat()
Log.d("CCAI", "Ended active chat before logout.")
}
} catch (e: Exception) {
// Session may not exist - that's fine
Log.w("CCAI", "No active session to end: ${e.message}")
}
// 2. Clear the cached auth token
// (prevents the next user from inheriting the previous session)
CCAI.authService?.updateAuthToken(null)
// 3. Wipe your own app's user data
MyAuthManager.clearLocalData()
// 4. Navigate back to the login screen
navigateToLoginScreen()
}
}
Kotlin 코루틴 및 Flow 통합
Android용 헤드리스 모바일 SDK는 Kotlin 코루틴과 Flow를 기본 반응형 패턴으로 설계되었습니다. iOS SDK의 Combine 통합에 상응하는 Android 통합입니다.
SDK 제공 항목: 관찰 가능한 반응형 스트림으로 실시간 SDK 이벤트 (수신 메시지, 화면 공유 상태 변경)를 내보내는 Kotlin 흐름 스트림 및 중단 함수
빌드하는 항목: 이러한 흐름을 수집하고 이에 따라 Android UI를 업데이트하는 코루틴 기반 통합
함수 정지
많은 SDK 서비스 메서드는 Kotlin 정지 함수입니다. 즉, 코루틴 범위 내에서 호출할 수 있으며 결과를 기다리는 동안 정지됩니다 (차단되지 않음).
import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope
// In an Activity or Fragment
lifecycleScope.launch {
try {
// These are suspend functions - they suspend, not block
val menuResult = CCAI.queueMenuService?.get()
CCAI.chatService?.start(chatRequest)
val company = CCAI.companyService?.get()
} catch (e: Exception) {
handleError(e)
}
}
채팅 서비스 흐름
chatService는 실시간 채팅 이벤트를 관찰하기 위한 Kotlin SharedFlow 속성을 노출합니다. 수명 주기 인식 코루틴 범위 (lifecycleScope 또는 viewModelScope) 내에서 수집합니다.
| Flow | 내보냄 |
|---|---|
messagesReceived |
수신 메시지 목록 (상담사, VA 또는 시스템에서 보낸 메시지) |
typingEvent |
사용자 또는 상담사가 입력 중인 경우 |
memberEvent |
사용자 또는 상담사가 채팅에 참여하거나 채팅에서 나가는 경우 (상담사 트랜스퍼 및 연결 해제 포함) |
stateChanged |
채팅 제공업체의 상태 변경 (연결 중, 연결됨, 연결 해제됨, 오류) |
chatReceived |
채팅을 수신하거나 업데이트할 때의 채팅 응답 데이터 (상태 변경, 상담사 할당, 세션 후 상태 포함) |
chatService는 전송 이벤트 (transferEventSubject), 상태 확인 결과 (checkStatusSubject), 메시지 전송 결과 (messageSendResultSubject)를 위한 추가 흐름을 노출합니다.
구현 예시
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
// 1. Incoming messages
lifecycleScope.launch {
CCAI.chatService?.messagesReceived?.collect { messages ->
for (message in messages) {
renderIncomingMessage(message)
}
}
}
// 2. Typing indicators
lifecycleScope.launch {
CCAI.chatService?.typingEvent?.collect { event ->
when (event) {
is ChatTypingEvent.Started -> showTypingIndicator()
is ChatTypingEvent.Ended -> hideTypingIndicator()
}
}
}
// 3. Member join/leave
lifecycleScope.launch {
CCAI.chatService?.memberEvent?.collect { event ->
when (event) {
is MemberEvent.Joined -> Log.d("Chat", "Member joined")
is MemberEvent.Left -> Log.d("Chat", "Member left")
}
// Refresh chat status after member changes
try { CCAI.chatService?.checkStatus() } catch (_: Exception) {}
}
}
// 4. Chat provider state changes
lifecycleScope.launch {
CCAI.chatService?.stateChanged?.distinctUntilChanged()?.collect { state ->
// Update UI based on connection state (examples: connecting, connected)
updateConnectionStatus(state)
}
}
// 5. Chat metadata updates (agent assignment, status changes)
lifecycleScope.launch {
CCAI.chatService?.chatReceived?.collect { chat ->
Log.d("Chat", "Received chat update: ${chat.id}")
updateAgentInfo(chat)
handlePostSessionIfNeeded(chat)
}
}
화면 공유 상태
SDK에서 제공하는 기능: 화면 공유 세션 상태는 ScreenShareManager.startSession()에 전달하는 ScreenShareCallbacks의 onSessionStateChanged 콜백을 통해 제공됩니다. Android용 헤드리스 모바일 SDK: 음성, 화면 공유, 예약된 통화, 이메일을 참고하세요. 이 콜백은 세션이 INACTIVE, PENDING, ACTIVE 사이에서 전환될 때마다 실행됩니다.
빌드하는 항목: 세션이 상태 간에 전환될 때 UI를 업데이트하는 ScreenShareCallbacks.onSessionStateChanged에 연결된 핸들러와 콜백 외부에서 현재 상태를 읽어야 하는 경우 로컬 속성 (예: currentScreenShareSessionState)
구현 예시
ScreenShareCallbacks 경로 외에도 ScreenShareManager는 독립형 addStateChangeListener / removeStateChangeListener API와 동기 getSessionState() 접근자를 노출합니다. startSession()를 호출한 구성요소 이외의 구성요소에서 상태를 관찰해야 하거나 일회성 비반응형 읽기를 실행해야 하는 경우 이를 사용합니다. 세션이 종료되면 콜백이 자동으로 해제되는 ScreenShareCallbacks 경로와 달리 독립형 리스너는 자동으로 해제되지 않습니다. 누수를 방지하려면 수명 주기 소유자의 해체에서 removeStateChangeListener(...)를 호출하세요.
// Register a state change listener
val listener: (ScreenShareSessionState) -> Unit = { state ->
when (state) {
ScreenShareSessionState.INACTIVE -> Log.d("ScreenShare", "No active session")
ScreenShareSessionState.PENDING -> Log.d("ScreenShare", "Session pending")
ScreenShareSessionState.ACTIVE -> Log.d("ScreenShare", "Session active")
}
}
ScreenShareManager.addStateChangeListener(listener)
// Get the current state directly (not reactive)
val currentState = ScreenShareManager.getSessionState()
// Clean up when done
ScreenShareManager.removeStateChangeListener(listener)
커스텀 데이터
SDK에서 제공하는 기능: 구조화된 메타데이터를 채팅 세션에 연결하는 메커니즘(서명된 JWT(백엔드에서 확인) 또는 서명되지 않은 페이로드(클라이언트에서 직접 설정)로 연결)
빌드하는 항목: 앱의 데이터 수집 로직과 서명된 데이터의 경우 백엔드 서버의 JWT 서명 엔드포인트 (전체 인증 및 JWT 페이로드 스키마는 Android용 헤드리스 모바일 SDK: 시작하기 참고)
서명된 맞춤 데이터
서명된 맞춤 데이터는 백엔드 서버에서 회사 보안 비밀로 서명된 JWT 토큰을 사용합니다. 이는 민감한 정보 또는 신뢰할 수 있는 데이터를 전달하는 안전한 접근 방식입니다.
구현 예시
백엔드에서 생성된 JWT에서 서명된 맞춤 데이터 객체를 만듭니다.
// JWT signed with company secret (signing must happen on your backend)
val customData = CustomData(signed = "header.payload.signature")
서명되지 않은 맞춤 데이터
서명되지 않은 맞춤 데이터는 CustomDataPayload 및 CustomDataItem를 사용하여 클라이언트에 직접 설정됩니다.
구현 예시
표시되는 데이터 항목과 에이전트 숨김 데이터 항목으로 서명되지 않은 페이로드를 빌드합니다. CustomDataPayload는 MutableMap<String, CustomDataItem?>를 사용하므로 아래 첨자 할당 (또는 mutableMapOf)을 사용하여 키별로 각 항목을 추가합니다. 선택한 키 (예: "user_name" 또는 "account_tier")는 상담사와 라우팅/분석에 표시되는 필드 이름이 됩니다.
val items = mutableMapOf<String, CustomDataItem?>()
items["user_name"] = CustomDataItem(
label = "Name",
value = "John"
)
items["user_age"] = CustomDataItem(
label = "Age",
value = 30,
type = CustomDataType.NUMBER,
invisibleToAgent = true
)
val payload = CustomDataPayload(items = items)
val customData = CustomData(unsigned = payload)
맞춤 데이터 항목은 다음 속성을 지원합니다.
label: 에이전트에게 표시되는 라벨입니다.value: 데이터 값 (예:String,Int,Bool)type: 데이터 유형 (STRING,NUMBER,DATE,URL,BOOLEAN)invisibleToAgent:true인 경우 데이터가 상담사에게는 숨겨지지만 라우팅 및 분석에는 사용할 수 있습니다.
채팅 요청에서 맞춤 데이터 사용
채팅 세션을 시작할 때 맞춤 데이터를 첨부합니다.
구현 예시
val chatRequest = ChatRequest(
chat = Chat(
menuId = 123,
languageCode = "en",
customData = customData
)
)
lifecycleScope.launch {
try {
CCAI.chatService?.start(chatRequest)
// Chat started - observe chat state using chatReceived
} catch (e: Exception) {
Log.e("CCAI", "Failed to start chat: ${e.message}")
}
}
외부 채팅 트랜스퍼
SDK에서 제공하는 항목: 이전 상담사의 이름과 대화 스크립트를 새 CCAI Platform 세션에 전달하는 데이터 구조 (ExternalChatTransfer)
빌드하는 항목: 외부 채팅 시스템에서 컨텍스트를 추출하여 SDK용으로 패키징하는 이전 흐름
구현 예시
이 코드는 이전 상담사의 이름으로 외부 채팅 트랜스퍼 객체를 만들고 세션의 맞춤 데이터에 연결합니다.
// ExternalChatAgent also accepts an optional `avatar: String? = null` parameter
// for displaying the previous agent's avatar, for example:
// ExternalChatAgent(name = "John Smith", avatar = "https://cdn.example.com/avatars/jsmith.png")
val transfer = ExternalChatTransfer(
agent = ExternalChatAgent(name = "John Smith"),
transcript = emptyList()
)
// Attach to your custom data payload
val payload = CustomDataPayload()
payload.externalChatTransfer = transfer
val customData = CustomData(unsigned = payload)
이는 서드 파티 채팅 시스템에서 CCAI 플랫폼 기반 세션으로 사용자를 이전할 때 수신 에이전트의 대화 컨텍스트를 유지하는 데 유용합니다.
스레딩 고려사항
Android용 헤드리스 모바일 SDK는 Kotlin 코루틴과 함께 작동하도록 설계되었으며 대부분 기본 안전합니다. 하지만 헤드리스 통합에는 중요한 스레딩 고려사항이 있습니다.
기본 스레드 안전
모든 SDK 메서드는 스레드 안전하며 모든 스레드에서 호출할 수 있습니다. 대부분의 SDK 서비스 호출 (예: chatService?.start(), queueMenuService?.get())은 내부적으로 자체 스레딩을 처리하는 정지 함수입니다. 스레드를 차단하지 않고 호출 코루틴을 정지합니다. 코루틴 범위 내에서 기본 스레드에서 안전하게 호출할 수 있습니다.
// Safe to call from the main thread
lifecycleScope.launch {
val menu = CCAI.queueMenuService?.get() // Suspends, does not block
updateUi(menu) // Back on main thread
}
CCAIDelegate 및 백그라운드 스레드
CCAIDelegate의 ccaiShouldAuthenticate() 메서드는 정지 함수이므로 이 함수 내에서 네트워크 호출을 안전하게 실행할 수 있습니다.
override suspend fun ccaiShouldAuthenticate(): String? {
// This runs in a coroutine context - safe to call suspend functions
return withContext(Dispatchers.IO) {
MyBackendApi.getSignedJwt() // Network call on IO dispatcher
}
}
SDK 콜백의 UI 업데이트
흐름을 사용하여 SDK 이벤트를 관찰할 때는 UI 업데이트가 기본 스레드에서 이루어지는지 확인하세요. lifecycleScope를 사용하는 경우 다음이 자동으로 처리됩니다.
// lifecycleScope dispatches to Main by default
lifecycleScope.launch {
chatMessagesFlow.collect { messages ->
// Already on Main - safe to update UI
chatAdapter.submitList(messages)
}
}
기본이 아닌 디스패처에서 수집하는 경우 명시적으로 다시 전환합니다.
lifecycleScope.launch(Dispatchers.IO) {
// Processing on IO thread
val processed = heavyProcessing()
withContext(Dispatchers.Main) {
// Update UI on Main thread
updateChatUi(processed)
}
}
초기화 스레딩
CCAI.initialize() 및 관련 모듈 초기화 호출(예: initializeChat(), initializeCall(), initializeScreenShare())은 Application.onCreate()의 기본 스레드에서 호출해야 합니다. 이는 SDK의 내부 상태를 설정하는 동기 호출입니다.
class MainApplication : Application() {
override fun onCreate() {
super.onCreate() // Always on main thread
// All initialization must happen on main thread
CCAI.initialize(context = this, options = initOptions)
CCAI.initializeChat(context = this, options = ChatOptions())
CCAI.initializeCall(context = this)
}
}
메모리 관리
적절한 메모리 관리는 누수를 방지하고 SDK 리소스의 완전한 해체를 보장하는 데 필수적입니다.
코루틴 및 흐름 수명 주기
Android 수명 주기 메서드 (활동의 경우 onDestroy(), ViewModel의 경우 onCleared())에서 항상 코루틴 Job 참조와 흐름 수집기를 취소하세요. 이렇게 하지 않으면 참조가 의도한 것보다 오래 유지되어 메모리 누수가 발생할 수 있습니다.
import kotlinx.coroutines.Job
class ChatViewModel : ViewModel() {
private var chatJob: Job? = null
fun observeChatMessages() {
chatJob = viewModelScope.launch {
CCAI.chatService?.messagesReceived?.collect { messages ->
// Handle new messages
}
}
}
override fun onCleared() {
super.onCleared()
chatJob?.cancel()
}
}
리스너 및 콜백 참조
리스너와 콜백에 약한 참조를 사용하여 보관 주기를 방지하세요. 활동 또는 ViewModel이 소멸되면 등록된 콜백을 삭제합니다.
class ChatActivity : AppCompatActivity() {
private var chatJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
chatJob = lifecycleScope.launch {
CCAI.chatService?.messagesReceived?.collect { messages ->
updateChatUI(messages)
}
}
}
override fun onDestroy() {
super.onDestroy()
chatJob?.cancel()
}
}
권장사항
코루틴
Job참조 저장: 범위를 벗어난 코루틴이 실행되지 않도록 항상launch {}에서 반환된Job참조를 저장합니다.수명 주기 메서드에서 정리:
onDestroy()(활동) 또는onCleared()(ViewModel)에서 모든 작업을 취소하고 콜백을 삭제합니다.리스너에 약한 참조 사용:
this(동봉된 활동 또는 프래그먼트 컨텍스트)를 암시적으로 캡처하는 람다는 가비지 컬렉션을 방지할 수 있으므로 주의해야 합니다.서비스가 SDK로 관리됨: SDK 서비스를 수동으로 출시할 필요가 없습니다.
CCAI싱글톤 객체로 관리됩니다.
문제 해결
이 섹션에서는 일반적인 통합 문제와 해결 방법을 다룹니다.
빌드 및 종속 항목 문제
이 섹션에서는 일반적인 빌드 및 종속 항목 문제를 다룹니다.
문제
Gradle에서 com.ccaiplatform.android:CCAIKit을 확인할 수 없습니다.
원인: CCAI Maven 저장소가 누락되었거나 잘못된 URL을 사용합니다.
수정:
settings.gradle.kts에 다음이 포함되어야 합니다.maven { url = uri("https://sdk.ujet.co/ccaip/android/") }/ccaip/경로 세그먼트를 확인합니다. 기존 URL (https://sdk.ujet.co/android/)은 아티팩트를 해결하지 않습니다.
문제
Java 17 필수 컴파일 오류
원인: 이 SDK에는 Java 17 호환성이 필요합니다.
해결:
build.gradle.kts업데이트compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" }
문제
런타임에 chatLibraryNotFound (오류 코드 1100)이 발생합니다.
원인: 종속 항목에서
CCAIChatRed모듈이 누락되었습니다.수정: 채팅에는
CCAIChat와CCAIChatRed가 모두 필요합니다. 추가:implementation("com.ccaiplatform.android:CCAIChat:ccaiVersion") implementation("com.ccaiplatform.android:CCAIChatRed:ccaiVersion")
초기화 문제
이 섹션에서는 일반적인 초기화 문제를 다룹니다.
문제
NullPointerExceptionCCAI.chatService에 액세스할 때
원인:
CCAI.initializeChat()가 호출되지 않았거나CCAI.initialize()전에 호출되었습니다.수정:
Application.onCreate()에서 초기화 순서가 다음과 같아야 합니다.CCAI.initialize(context, options = initOptions)CCAI.initializeChat(context, options = chatOptions)CCAI.initializeCall(context)(음성 또는 예약된 통화를 사용하는 경우)CCAI.initializeScreenShare(context, options)(화면 공유를 사용하는 경우)
문제
SDK 서비스는 초기화 후 null를 반환합니다.
원인:
CCAI.initialize()가 기본 스레드에서 호출되지 않았거나Application클래스가 매니페스트에 등록되지 않았습니다.수정:
AndroidManifest.xml파일에android:name=".MainApplication"줄이 있고 모든 init 호출이onCreate()에 있는지 확인합니다.
인증 문제
이 섹션에서는 일반적인 인증 문제를 다룹니다.
문제
인증이 자동으로 실패하거나 ccaiShouldAuthenticate()가 호출되지 않습니다.
원인:
InitOptions에서CCAIDelegate가 설정되지 않았습니다.수정:
delegate가InitOptions에 전달되는지 확인합니다.val initOptions = InitOptions( key = "YOUR_KEY", urlHost = "your_subdomain.ccaiplatform.com", delegate = MyCCAIDelegate() // Must not be null )
문제
JWT 검증 오류 (오류 코드 101)
원인: JWT가 만료되었거나, 형식이 잘못되었거나, 잘못된 보안 비밀로 서명되었습니다.
수정: 백엔드가 다음을 충족하는지 확인합니다.
올바른 회사 비밀 코드를 사용합니다.
유효한
iat및exp타임스탬프를 설정합니다.올바른 형식의 JWT 문자열을 반환합니다.
푸시 알림 문제
이 섹션에서는 일반적인 푸시 알림 문제를 다룹니다.
문제
푸시 알림이 수신되거나 처리되지 않습니다.
원인: FCM 토큰이 SDK에 등록되지 않았거나, Firebase 메시지 서비스가 등록되지 않았거나, 수신 데이터 페이로드가 SDK로 전달되지 않았습니다.
문제 해결:
onNewToken()과 시작 시 모두에서CCAI.pushNotificationService?.updatePushToken(token)를 호출합니다.Firebase 메시징 서비스에서
remoteMessage.data를CCAI.pushNotificationService?.handlePushNotification(remoteMessage.data)로 전달합니다.google-services.json가 앱 모듈에 있는지 확인합니다.CCAI 플랫폼 관리자 포털에서 Firebase 서비스 계정 키가 구성되어 있는지 확인합니다.
자체 앱 코드에서 표시되는 알림 또는 수신 전화 UI를 렌더링합니다.
문제
SDK가 수신 푸시 알림에 반응하지 않습니다 (메시지가 무시됨).
원인:
handlePushNotification()메서드에 잘못된 데이터 유형을 전달합니다.수정: 전체
RemoteMessage객체가 아닌remoteMessage.data(Map<String, String>)를 전달해야 합니다.
채팅 세션 문제
이 섹션에서는 일반적인 채팅 세션 문제를 다룹니다.
문제
chatService?.start()에서 예외를 발생시키거나 null을 반환합니다.
원인:
menuId이 잘못되었거나 인증 실패 또는 네트워크 문제가 있습니다.문제 해결:
menuId이 관리자 포털의 유효한 대기열과 일치하는지 확인합니다.ccaiShouldAuthenticate()가CCAI.authService?.authenticate(jwt)를 통해 유효한 서명된 JWT를 교환하고 결과 인증 토큰을 반환하는지 확인합니다.기기에 네트워크 연결이 있는지 확인합니다.
try 또는 catch로 호출을 래핑한 다음 예외 세부정보를 로깅합니다.
문제
채팅 메시지가 UI에 표시되지 않음
원인: 채팅 서비스의 메시지 스트림을 관찰하지 않습니다.
수정: 수명 주기 인식 범위에서 채팅 서비스의 흐름을 수집해야 합니다 (위의 채팅 서비스 흐름 섹션 참고).
화면 공유 문제
이 섹션에서는 일반적인 화면 공유 문제를 다룹니다.
문제
사용자가 동의를 부여한 후 화면 공유가 시작되지 않습니다.
원인:
CCAIScreenShare모듈이 초기화되지 않았거나 AndroidMediaProjection가 시작되지 않았습니다.문제 해결:
CCAI.initializeScreenShare()가 유효한ScreenShareOptions로 호출되는지 확인합니다.동의 후 Android의
MediaProjectionManager및 포그라운드 서비스를 올바르게 시작하는지 확인합니다.Android 14 이상에서는 포그라운드 서비스 유형에
mediaProjection가 포함되어야 합니다.
일반적인 디버깅 도움말
개발 중에 상세 로깅을 사용 설정하여 SDK 내부 상태를 확인하세요.
CCAI Platform 관리자 포털에서 대기열 구성, 채널 사용 설정, 운영 시간을 확인합니다.
Charles Proxy 또는 Android 스튜디오의 네트워크 검사기와 같은 도구로 네트워크 트래픽을 모니터링하여
*.ccaiplatform.com에 대한 API 호출을 확인합니다.모듈 버전 일치 확인: 호환성 문제를 방지하려면 모든
com.ccaiplatform.android모듈이 동일한 버전 번호를 사용해야 합니다.
시리즈 마무리
축하합니다. Android용 헤드리스 모바일 SDK 5부작을 완료했습니다. 이제 통합에서 다음을 지원합니다.
설정 및 초기화 (Android용 헤드리스 모바일 SDK: 시작하기)
대기열, 채널, 채팅 (Android용 헤드리스 모바일 SDK: 대기열, 채널, 채팅)
음성, 화면 공유, 예약된 통화, 이메일 (Android용 헤드리스 모바일 SDK: 음성, 화면 공유, 예약된 통화, 이메일)
스마트 액션, 첨부파일, 전환 (Android용 헤드리스 모바일 SDK: 스마트 액션, 첨부파일, 전환)
세션 후 흐름, 현지화, 오류 처리, 고급 주제 (이 도움말)
지속적으로 참고하려면 헤드리스 모바일 SDK - Android API 참조를 북마크하고 SDK 업데이트에 관한 CCAI 플랫폼 출시 노트를 확인하세요.