From f71ce7f9b16d87580c8388e3ede426d19b17615e Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Tue, 4 Aug 2026 02:47:42 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20#72=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20+=20ChatRepo?= =?UTF-8?q?sitory=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- .../kikidan/domain/model/chat/ChatAction.kt | 14 ++++++++++ .../kikidan/domain/model/chat/ChatEntry.kt | 21 +++++++++++++++ .../kikidan/domain/model/chat/ChatMessage.kt | 23 ++++++++++++++++ .../domain/model/chat/ChatStreamEvent.kt | 27 +++++++++++++++++++ .../kikidan/domain/model/chat/Conversation.kt | 16 +++++++++++ .../domain/repository/ChatRepository.kt | 23 ++++++++++++++++ 6 files changed, 124 insertions(+) create mode 100644 core/domain/src/main/java/com/kikidan/domain/model/chat/ChatAction.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/model/chat/ChatMessage.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/model/chat/Conversation.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/repository/ChatRepository.kt diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatAction.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatAction.kt new file mode 100644 index 0000000..0e91be0 --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatAction.kt @@ -0,0 +1,14 @@ +package com.kikidan.domain.model.chat + +import java.time.LocalDate + +data class ChatAction( + val type: ChatActionType, + val label: String, + val category: String, + val date: LocalDate?, +) + +enum class ChatActionType { + CALENDAR_ADD, +} diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt new file mode 100644 index 0000000..a602ba1 --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt @@ -0,0 +1,21 @@ +package com.kikidan.domain.model.chat + +data class ChatEntry( + val greeting: String, + val suggestions: List, + val quota: ChatQuota, +) + +data class ChatSuggestion( + val emoji: String, + val label: String, + val seedPrompt: String, + val category: String, +) + +data class ChatQuota( + val used: Int, + val limit: Int, +) { + val remaining: Int get() = (limit - used).coerceAtLeast(0) +} diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatMessage.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatMessage.kt new file mode 100644 index 0000000..b3a137f --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatMessage.kt @@ -0,0 +1,23 @@ +package com.kikidan.domain.model.chat + +import java.time.Instant + +data class ChatMessage( + val id: String, + val role: MessageRole, + val content: String, + val status: MessageStatus, + val action: ChatAction?, + val createdAt: Instant, +) + +enum class MessageRole { + USER, + ASSISTANT, +} + +enum class MessageStatus { + GENERATING, + COMPLETED, + FAILED, +} diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt new file mode 100644 index 0000000..df2d084 --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt @@ -0,0 +1,27 @@ +package com.kikidan.domain.model.chat + +sealed interface ChatStreamEvent { + data class Start( + val conversationId: String, + val userMessageId: String, + val assistantMessageId: String, + val quota: ChatQuota, + ) : ChatStreamEvent + + data class Delta( + val text: String, + ) : ChatStreamEvent + + data class Action( + val action: ChatAction, + ) : ChatStreamEvent + + data class Done( + val assistantMessageId: String, + ) : ChatStreamEvent +} + +class ChatStreamException( + val code: String?, + override val message: String, +) : Exception(message) diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/Conversation.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/Conversation.kt new file mode 100644 index 0000000..128c5a2 --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/Conversation.kt @@ -0,0 +1,16 @@ +package com.kikidan.domain.model.chat + +import java.time.Instant + +data class Conversation( + val id: String, + val title: String, + val messages: List, +) + +data class ConversationSummary( + val id: String, + val title: String, + val lastMessageAt: Instant, + val unread: Boolean, +) diff --git a/core/domain/src/main/java/com/kikidan/domain/repository/ChatRepository.kt b/core/domain/src/main/java/com/kikidan/domain/repository/ChatRepository.kt new file mode 100644 index 0000000..1e9a49d --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/repository/ChatRepository.kt @@ -0,0 +1,23 @@ +package com.kikidan.domain.repository + +import com.kikidan.domain.model.chat.ChatEntry +import com.kikidan.domain.model.chat.ChatStreamEvent +import com.kikidan.domain.model.chat.Conversation +import com.kikidan.domain.model.chat.ConversationSummary +import kotlinx.coroutines.flow.Flow + +interface ChatRepository { + suspend fun getChatEntry(): Result + + // conversationId가 null이면 새 대화를 시작한다. 새 대화의 id는 첫 Start 이벤트로 내려온다. + fun sendMessage( + conversationId: String?, + content: String, + ): Flow> + + suspend fun getConversations(): Result> + + suspend fun getConversationDetail(conversationId: String): Result + + suspend fun deleteConversation(conversationId: String): Result +} From 97102c34d7e6deed937ca795198889384df01ba2 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 5 Aug 2026 02:05:21 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20ChatStreamEvent=EC=97=90=20Error=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20Exception?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatStreamException을 제거하고 ChatStreamEvent의 하위 타입으로 Error를 추가한다. 스트림 처리 중 발생하는 에러를 별도 예외(Exception) 대신 이벤트의 일부로 포함하여 명시적으로 관리한다. --- .../com/kikidan/domain/model/chat/ChatStreamEvent.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt index df2d084..d52f19b 100644 --- a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt @@ -19,9 +19,9 @@ sealed interface ChatStreamEvent { data class Done( val assistantMessageId: String, ) : ChatStreamEvent -} -class ChatStreamException( - val code: String?, - override val message: String, -) : Exception(message) + data class Error( + val code: String, + val message: String + ) +} From da3b9c3818e9f49c462a570dc4e7edbb1b23a22b Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Tue, 4 Aug 2026 03:02:25 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20#73=20=EC=B1=84=ED=8C=85=20Domain?= =?UTF-8?q?=20UseCase=205=EC=A2=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- core/domain/build.gradle.kts | 2 + .../usecase/DeleteConversationUseCase.kt | 13 ++++ .../domain/usecase/GetChatEntryUseCase.kt | 13 ++++ .../usecase/GetConversationDetailUseCase.kt | 14 ++++ .../domain/usecase/GetConversationsUseCase.kt | 13 ++++ .../domain/usecase/SendChatMessageUseCase.kt | 33 +++++++++ .../kikidan/domain/fake/FakeChatRepository.kt | 38 ++++++++++ .../usecase/SendChatMessageUseCaseTest.kt | 70 +++++++++++++++++++ 8 files changed, 196 insertions(+) create mode 100644 core/domain/src/main/java/com/kikidan/domain/usecase/DeleteConversationUseCase.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/usecase/GetChatEntryUseCase.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/usecase/GetConversationDetailUseCase.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/usecase/GetConversationsUseCase.kt create mode 100644 core/domain/src/main/java/com/kikidan/domain/usecase/SendChatMessageUseCase.kt create mode 100644 core/domain/src/test/java/com/kikidan/domain/fake/FakeChatRepository.kt create mode 100644 core/domain/src/test/java/com/kikidan/domain/usecase/SendChatMessageUseCaseTest.kt diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index da4e69e..f487d27 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -15,4 +15,6 @@ kotlin { dependencies { implementation(libs.javax.inject) api(libs.kotlinx.coroutines.core) + testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) } diff --git a/core/domain/src/main/java/com/kikidan/domain/usecase/DeleteConversationUseCase.kt b/core/domain/src/main/java/com/kikidan/domain/usecase/DeleteConversationUseCase.kt new file mode 100644 index 0000000..e3dd03a --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/usecase/DeleteConversationUseCase.kt @@ -0,0 +1,13 @@ +package com.kikidan.domain.usecase + +import com.kikidan.domain.repository.ChatRepository +import javax.inject.Inject + +class DeleteConversationUseCase + @Inject + constructor( + private val chatRepository: ChatRepository, + ) { + suspend operator fun invoke(conversationId: String): Result = + chatRepository.deleteConversation(conversationId) + } diff --git a/core/domain/src/main/java/com/kikidan/domain/usecase/GetChatEntryUseCase.kt b/core/domain/src/main/java/com/kikidan/domain/usecase/GetChatEntryUseCase.kt new file mode 100644 index 0000000..9c5cea7 --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/usecase/GetChatEntryUseCase.kt @@ -0,0 +1,13 @@ +package com.kikidan.domain.usecase + +import com.kikidan.domain.model.chat.ChatEntry +import com.kikidan.domain.repository.ChatRepository +import javax.inject.Inject + +class GetChatEntryUseCase + @Inject + constructor( + private val chatRepository: ChatRepository, + ) { + suspend operator fun invoke(): Result = chatRepository.getChatEntry() + } diff --git a/core/domain/src/main/java/com/kikidan/domain/usecase/GetConversationDetailUseCase.kt b/core/domain/src/main/java/com/kikidan/domain/usecase/GetConversationDetailUseCase.kt new file mode 100644 index 0000000..f1a9f3e --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/usecase/GetConversationDetailUseCase.kt @@ -0,0 +1,14 @@ +package com.kikidan.domain.usecase + +import com.kikidan.domain.model.chat.Conversation +import com.kikidan.domain.repository.ChatRepository +import javax.inject.Inject + +class GetConversationDetailUseCase + @Inject + constructor( + private val chatRepository: ChatRepository, + ) { + suspend operator fun invoke(conversationId: String): Result = + chatRepository.getConversationDetail(conversationId) + } diff --git a/core/domain/src/main/java/com/kikidan/domain/usecase/GetConversationsUseCase.kt b/core/domain/src/main/java/com/kikidan/domain/usecase/GetConversationsUseCase.kt new file mode 100644 index 0000000..4878062 --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/usecase/GetConversationsUseCase.kt @@ -0,0 +1,13 @@ +package com.kikidan.domain.usecase + +import com.kikidan.domain.model.chat.ConversationSummary +import com.kikidan.domain.repository.ChatRepository +import javax.inject.Inject + +class GetConversationsUseCase + @Inject + constructor( + private val chatRepository: ChatRepository, + ) { + suspend operator fun invoke(): Result> = chatRepository.getConversations() + } diff --git a/core/domain/src/main/java/com/kikidan/domain/usecase/SendChatMessageUseCase.kt b/core/domain/src/main/java/com/kikidan/domain/usecase/SendChatMessageUseCase.kt new file mode 100644 index 0000000..f9c41f6 --- /dev/null +++ b/core/domain/src/main/java/com/kikidan/domain/usecase/SendChatMessageUseCase.kt @@ -0,0 +1,33 @@ +package com.kikidan.domain.usecase + +import com.kikidan.domain.model.chat.ChatStreamEvent +import com.kikidan.domain.repository.ChatRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class SendChatMessageUseCase + @Inject + constructor( + private val chatRepository: ChatRepository, + ) { + operator fun invoke( + conversationId: String?, + content: String, + ): Flow> { + val trimmed = content.trim() + if (trimmed.isEmpty() || trimmed.length > MAX_CONTENT_LENGTH) { + return flowOf( + Result.failure( + IllegalArgumentException("메시지는 1자 이상 ${MAX_CONTENT_LENGTH}자 이하여야 합니다."), + ), + ) + } + return chatRepository.sendMessage(conversationId, trimmed) + } + + companion object { + // presentation의 입력창 maxLength도 이 상수를 참조한다. + const val MAX_CONTENT_LENGTH = 500 + } + } diff --git a/core/domain/src/test/java/com/kikidan/domain/fake/FakeChatRepository.kt b/core/domain/src/test/java/com/kikidan/domain/fake/FakeChatRepository.kt new file mode 100644 index 0000000..684b384 --- /dev/null +++ b/core/domain/src/test/java/com/kikidan/domain/fake/FakeChatRepository.kt @@ -0,0 +1,38 @@ +package com.kikidan.domain.fake + +import com.kikidan.domain.model.chat.ChatEntry +import com.kikidan.domain.model.chat.ChatStreamEvent +import com.kikidan.domain.model.chat.Conversation +import com.kikidan.domain.model.chat.ConversationSummary +import com.kikidan.domain.repository.ChatRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow + +class FakeChatRepository : ChatRepository { + var lastSentConversationId: String? = null + var lastSentContent: String? = null + var sendMessageCallCount: Int = 0 + var streamEvents: List> = emptyList() + + override suspend fun getChatEntry(): Result = + Result.failure(NotImplementedError()) + + override fun sendMessage( + conversationId: String?, + content: String, + ): Flow> { + lastSentConversationId = conversationId + lastSentContent = content + sendMessageCallCount++ + return streamEvents.asFlow() + } + + override suspend fun getConversations(): Result> = + Result.failure(NotImplementedError()) + + override suspend fun getConversationDetail(conversationId: String): Result = + Result.failure(NotImplementedError()) + + override suspend fun deleteConversation(conversationId: String): Result = + Result.failure(NotImplementedError()) +} diff --git a/core/domain/src/test/java/com/kikidan/domain/usecase/SendChatMessageUseCaseTest.kt b/core/domain/src/test/java/com/kikidan/domain/usecase/SendChatMessageUseCaseTest.kt new file mode 100644 index 0000000..8f8333b --- /dev/null +++ b/core/domain/src/test/java/com/kikidan/domain/usecase/SendChatMessageUseCaseTest.kt @@ -0,0 +1,70 @@ +package com.kikidan.domain.usecase + +import com.kikidan.domain.fake.FakeChatRepository +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class SendChatMessageUseCaseTest { + private lateinit var fakeRepository: FakeChatRepository + private lateinit var useCase: SendChatMessageUseCase + + @Before + fun setUp() { + fakeRepository = FakeChatRepository() + useCase = SendChatMessageUseCase(fakeRepository) + } + + @Test + fun `정상 content 전송 시 Repository Flow가 그대로 전달된다`() = runTest { + val results = useCase(conversationId = null, content = "안녕하세요").toList() + assertEquals(1, fakeRepository.sendMessageCallCount) + assertEquals("안녕하세요", fakeRepository.lastSentContent) + } + + @Test + fun `content가 빈 문자열이면 Repository 호출 없이 failure를 방출한다`() = runTest { + val results = useCase(conversationId = null, content = "").toList() + assertEquals(0, fakeRepository.sendMessageCallCount) + assertEquals(1, results.size) + assertTrue(results[0].isFailure) + assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) + } + + @Test + fun `content가 공백만이면 Repository 호출 없이 failure를 방출한다`() = runTest { + val results = useCase(conversationId = null, content = " ").toList() + assertEquals(0, fakeRepository.sendMessageCallCount) + assertEquals(1, results.size) + assertTrue(results[0].isFailure) + assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) + } + + @Test + fun `content가 501자이면 Repository 호출 없이 failure를 방출한다`() = runTest { + val content = "a".repeat(SendChatMessageUseCase.MAX_CONTENT_LENGTH + 1) + val results = useCase(conversationId = null, content = content).toList() + assertEquals(0, fakeRepository.sendMessageCallCount) + assertEquals(1, results.size) + assertTrue(results[0].isFailure) + assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) + } + + @Test + fun `content 앞뒤 공백은 trim되어 Repository에 전달된다`() = runTest { + useCase(conversationId = null, content = " 안녕 ").toList() + assertEquals(1, fakeRepository.sendMessageCallCount) + assertEquals("안녕", fakeRepository.lastSentContent) + } + + @Test + fun `content가 정확히 500자이면 정상 통과한다`() = runTest { + val content = "a".repeat(SendChatMessageUseCase.MAX_CONTENT_LENGTH) + useCase(conversationId = null, content = content).toList() + assertEquals(1, fakeRepository.sendMessageCallCount) + assertEquals(content, fakeRepository.lastSentContent) + } +} From fa6c357bf699378d8416279c0d029b07140e3374 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 5 Aug 2026 02:24:29 +0900 Subject: [PATCH 4/7] =?UTF-8?q?style:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=BD=94=EB=93=9C=20=ED=8F=AC=EB=A7=B7?= =?UTF-8?q?=ED=8C=85=20=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SendChatMessageUseCaseTest`의 `runTest` 블록 들여쓰기와 줄바꿈을 일관되게 수정하고, `ChatStreamEvent`에 trailing comma를 추가하여 스타일 가이드를 준수한다. `FakeChatRepository`에서 단순 failure를 반환하는 메서드들을 단일 행으로 정리하여 가독성을 높인다. --- .../domain/model/chat/ChatStreamEvent.kt | 2 +- .../kikidan/domain/fake/FakeChatRepository.kt | 6 +- .../usecase/SendChatMessageUseCaseTest.kt | 82 ++++++++++--------- 3 files changed, 47 insertions(+), 43 deletions(-) diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt index d52f19b..d5477ad 100644 --- a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt @@ -22,6 +22,6 @@ sealed interface ChatStreamEvent { data class Error( val code: String, - val message: String + val message: String, ) } diff --git a/core/domain/src/test/java/com/kikidan/domain/fake/FakeChatRepository.kt b/core/domain/src/test/java/com/kikidan/domain/fake/FakeChatRepository.kt index 684b384..d443c6c 100644 --- a/core/domain/src/test/java/com/kikidan/domain/fake/FakeChatRepository.kt +++ b/core/domain/src/test/java/com/kikidan/domain/fake/FakeChatRepository.kt @@ -14,8 +14,7 @@ class FakeChatRepository : ChatRepository { var sendMessageCallCount: Int = 0 var streamEvents: List> = emptyList() - override suspend fun getChatEntry(): Result = - Result.failure(NotImplementedError()) + override suspend fun getChatEntry(): Result = Result.failure(NotImplementedError()) override fun sendMessage( conversationId: String?, @@ -27,8 +26,7 @@ class FakeChatRepository : ChatRepository { return streamEvents.asFlow() } - override suspend fun getConversations(): Result> = - Result.failure(NotImplementedError()) + override suspend fun getConversations(): Result> = Result.failure(NotImplementedError()) override suspend fun getConversationDetail(conversationId: String): Result = Result.failure(NotImplementedError()) diff --git a/core/domain/src/test/java/com/kikidan/domain/usecase/SendChatMessageUseCaseTest.kt b/core/domain/src/test/java/com/kikidan/domain/usecase/SendChatMessageUseCaseTest.kt index 8f8333b..e933c7b 100644 --- a/core/domain/src/test/java/com/kikidan/domain/usecase/SendChatMessageUseCaseTest.kt +++ b/core/domain/src/test/java/com/kikidan/domain/usecase/SendChatMessageUseCaseTest.kt @@ -19,52 +19,58 @@ class SendChatMessageUseCaseTest { } @Test - fun `정상 content 전송 시 Repository Flow가 그대로 전달된다`() = runTest { - val results = useCase(conversationId = null, content = "안녕하세요").toList() - assertEquals(1, fakeRepository.sendMessageCallCount) - assertEquals("안녕하세요", fakeRepository.lastSentContent) - } + fun `정상 content 전송 시 Repository Flow가 그대로 전달된다`() = + runTest { + val results = useCase(conversationId = null, content = "안녕하세요").toList() + assertEquals(1, fakeRepository.sendMessageCallCount) + assertEquals("안녕하세요", fakeRepository.lastSentContent) + } @Test - fun `content가 빈 문자열이면 Repository 호출 없이 failure를 방출한다`() = runTest { - val results = useCase(conversationId = null, content = "").toList() - assertEquals(0, fakeRepository.sendMessageCallCount) - assertEquals(1, results.size) - assertTrue(results[0].isFailure) - assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) - } + fun `content가 빈 문자열이면 Repository 호출 없이 failure를 방출한다`() = + runTest { + val results = useCase(conversationId = null, content = "").toList() + assertEquals(0, fakeRepository.sendMessageCallCount) + assertEquals(1, results.size) + assertTrue(results[0].isFailure) + assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) + } @Test - fun `content가 공백만이면 Repository 호출 없이 failure를 방출한다`() = runTest { - val results = useCase(conversationId = null, content = " ").toList() - assertEquals(0, fakeRepository.sendMessageCallCount) - assertEquals(1, results.size) - assertTrue(results[0].isFailure) - assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) - } + fun `content가 공백만이면 Repository 호출 없이 failure를 방출한다`() = + runTest { + val results = useCase(conversationId = null, content = " ").toList() + assertEquals(0, fakeRepository.sendMessageCallCount) + assertEquals(1, results.size) + assertTrue(results[0].isFailure) + assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) + } @Test - fun `content가 501자이면 Repository 호출 없이 failure를 방출한다`() = runTest { - val content = "a".repeat(SendChatMessageUseCase.MAX_CONTENT_LENGTH + 1) - val results = useCase(conversationId = null, content = content).toList() - assertEquals(0, fakeRepository.sendMessageCallCount) - assertEquals(1, results.size) - assertTrue(results[0].isFailure) - assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) - } + fun `content가 501자이면 Repository 호출 없이 failure를 방출한다`() = + runTest { + val content = "a".repeat(SendChatMessageUseCase.MAX_CONTENT_LENGTH + 1) + val results = useCase(conversationId = null, content = content).toList() + assertEquals(0, fakeRepository.sendMessageCallCount) + assertEquals(1, results.size) + assertTrue(results[0].isFailure) + assertTrue(results[0].exceptionOrNull() is IllegalArgumentException) + } @Test - fun `content 앞뒤 공백은 trim되어 Repository에 전달된다`() = runTest { - useCase(conversationId = null, content = " 안녕 ").toList() - assertEquals(1, fakeRepository.sendMessageCallCount) - assertEquals("안녕", fakeRepository.lastSentContent) - } + fun `content 앞뒤 공백은 trim되어 Repository에 전달된다`() = + runTest { + useCase(conversationId = null, content = " 안녕 ").toList() + assertEquals(1, fakeRepository.sendMessageCallCount) + assertEquals("안녕", fakeRepository.lastSentContent) + } @Test - fun `content가 정확히 500자이면 정상 통과한다`() = runTest { - val content = "a".repeat(SendChatMessageUseCase.MAX_CONTENT_LENGTH) - useCase(conversationId = null, content = content).toList() - assertEquals(1, fakeRepository.sendMessageCallCount) - assertEquals(content, fakeRepository.lastSentContent) - } + fun `content가 정확히 500자이면 정상 통과한다`() = + runTest { + val content = "a".repeat(SendChatMessageUseCase.MAX_CONTENT_LENGTH) + useCase(conversationId = null, content = content).toList() + assertEquals(1, fakeRepository.sendMessageCallCount) + assertEquals(content, fakeRepository.lastSentContent) + } } From 8f83129a37dfc0eb79df27e062d8c14d0ecc1ed6 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 5 Aug 2026 17:21:04 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20ChatStreamEvent.Error=20=EC=83=81?= =?UTF-8?q?=EC=86=8D=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Error 데이터 클래스가 ChatStreamEvent를 상속하도록 수정하여 채팅 스트림 이벤트 처리 시 타입 일관성을 유지하고 에러 상태를 정상적으로 포함할 수 있도록 한다. --- .../main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt index d5477ad..b959fc7 100644 --- a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatStreamEvent.kt @@ -23,5 +23,5 @@ sealed interface ChatStreamEvent { data class Error( val code: String, val message: String, - ) + ) : ChatStreamEvent } From 7fb38353e20314b604fa7e4badf9eb4c3b20e73e Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Thu, 6 Aug 2026 01:59:00 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20ChatEntry=EC=9D=98=20category?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20nullable=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카테고리 정보가 없는 채팅 엔트리를 지원하기 위해 `category` 타입을 String?로 변경한다. --- .../src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt index a602ba1..ff8b264 100644 --- a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt @@ -10,7 +10,7 @@ data class ChatSuggestion( val emoji: String, val label: String, val seedPrompt: String, - val category: String, + val category: String?, ) data class ChatQuota( From 91d5dac1c6a1890f4df4202eb226b9fbae92ab34 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Thu, 6 Aug 2026 16:13:05 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20ChatEntry=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=ED=83=80=EC=9E=85=EC=9D=84=20Enum?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 String 타입이었던 category를 ChatCategory Enum으로 변경하여 타입 안정성을 확보한다. 카테고리는 RELATIONSHIP, LOVE, ACHIEVEMENT, MONEY, HEALTH의 5가지 항목으로 정의한다. --- .../java/com/kikidan/domain/model/chat/ChatEntry.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt index ff8b264..8bb438e 100644 --- a/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt +++ b/core/domain/src/main/java/com/kikidan/domain/model/chat/ChatEntry.kt @@ -10,7 +10,7 @@ data class ChatSuggestion( val emoji: String, val label: String, val seedPrompt: String, - val category: String?, + val category: ChatCategory?, ) data class ChatQuota( @@ -19,3 +19,11 @@ data class ChatQuota( ) { val remaining: Int get() = (limit - used).coerceAtLeast(0) } + +enum class ChatCategory { + RELATIONSHIP, + LOVE, + ACHIEVEMENT, + MONEY, + HEALTH, +}