Skip to content

[Feat] ChatViewModel 및 TyprWriterFlow 작성 - #86

Draft
oungsi2000 wants to merge 9 commits into
feat/76-chat-data-layerfrom
feat/77-chat-viewmodel
Draft

[Feat] ChatViewModel 및 TyprWriterFlow 작성#86
oungsi2000 wants to merge 9 commits into
feat/76-chat-data-layerfrom
feat/77-chat-viewmodel

Conversation

@oungsi2000

@oungsi2000 oungsi2000 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

관련 이슈

#77

작업 내용

  • 토닥이 채팅 ViewModel을 작성하였습니다
  • 서버 스펙이 SSE로 정해짐에 따라 한 글자 한 글자씩 렌더링되게 하는 flow인 TypeWriterFlow를 작성하였습니다

변경사항 / 상세

  • IO 스레드에서 별도로 서버에서 출력 buffer를 받고, 매 프레임마다 buffer.size - 이미 화면에 노출한 글자수의 / 32 만큼을 매 프레임마다 방출합니다.

중점 리뷰사항

스크린샷 (선택)

Summary by CodeRabbit

  • 새로운 기능

    • 채팅 화면의 초기 진입, 기존 대화 불러오기, 새 대화 시작을 지원합니다.
    • 메시지 입력·전송과 추천 문구 선택 기능을 제공합니다.
    • 답변을 실시간 스트리밍으로 표시하고 타자 효과를 적용합니다.
    • 전송 중 상태, 사용량 한도, 오류 및 취소 상황을 처리합니다.
  • 테스트

    • 채팅 로드, 메시지 전송, 스트리밍, 오류 처리, 입력 제한 등 주요 동작을 검증하는 테스트를 추가했습니다.
    • 타자 효과와 스트리밍 취소·예외 상황에 대한 테스트를 추가했습니다.

TypewriterFlowTest의 취소 테스트에서 runTest 기본 디스패처를
StandardTestDispatcher가 아닌 UnconfinedTestDispatcher로 오인해
job이 시작되기 전에 취소되던 버그를 runCurrent() 추가로 수정.
ChatSideEffect.ShowMessage를 제거하고, 예외 객체를 직접 전달하는 Error와 스트리밍 전용 에러인 ShowStreamingErrorMessage로 세분화하여 에러 처리 로직을 개선한다.
채팅 스트림에서 ChatStreamEvent.Error 이벤트를 직접 처리하도록 로직을 추가한다.

사용자 메시지 생성 시 초기 상태를 PENDING에서 COMPLETED로 변경하여 낙관적 업데이트 UI를 조정한다.
또한, 테스트 코드를 포함한 feature/chat 모듈 전반에 ktlint 포맷팅을 적용하고 코드 구조를 정리한다.
기존의 `ChatPhase` enum과 `streamingText` 필드를 `StreamingChatState` sealed interface로 통합한다.
상태에 따라 필요한 데이터(`streamingText`)를 `Typing` 상태 내부에 캡슐화하여, Idle이나 Thinking 상태에서 불필요한 데이터에 접근하는 것을 방지하고 상태 전이를 명확하게 표현한다.

- `ChatContract.kt`를 제거하고 모델들을 `model` 패키지로 이동
- `ChatViewModel` 내 상태 업데이트 및 메시지 전송 로직을 변경된 상태 모델에 맞춰 수정
- `StreamingChatState.Typing`인 경우에만 `streamingText`를 참조하도록 보장하여 안정성 개선
ChatViewModel의 인덴트와 메서드 체이닝 줄바꿈을 일관되게 수정한다.
ChatState 및 하위 데이터 클래스에 trailing comma를 추가하여 가독성을 개선하고 향후 변경 시 diff 노이즈를 방지한다.
`send()` 함수에서 스트리밍 중인 경우 요청을 무시하는 가드 로직이 반대로 되어 있어, `Idle` 상태가 아닐 때 리턴하도록 수정한다.
테스트 코드의 상태 검증 로직을 `ChatPhase`에서 `StreamingChatState` 기반으로 변경하고, 가독성을 위해 헬퍼 메서드 명칭과 테스트 이름을 정리한다.
ChatViewModel의 send 메서드 내 스트리밍 시작 및 종료 처리 로직을 onStreamingStart, onSteamingDone으로 분리하여 가독성을 개선했다. 기존에 존재하던 할당량(quota) 초과 가드 로직은 제거되었다.

TypewriterFlow는 MutableStateFlow 대신 StringBuffer를 사용하도록 변경하여 문자열 결합 효율을 높였으며, CATCH_UP_DIVISOR 값을 32로 조정하여 실제 기기 체감에 맞춰 타이핑 방출 속도를 최적화했다. 테스트 코드에서는 불필요한 문자열 템플릿과 공백을 정리했다.
ChatSuggestion의 카테고리 인자가 String에서 ChatCategory 타입으로 변경됨에 따라 테스트 데이터를 수정한다.
잔여 할당량이 0일 때 메시지 전송을 차단하던 로직의 테스트 케이스를 삭제한다.
TypewriterFlowTest에 ExperimentalCoroutinesApi 사용을 위한 @OptIn 설정을 추가한다.
메시지 플레이스홀더를 생성할 때 `trim()`을 호출하여
입력된 텍스트의 불필요한 앞뒤 공백을 제거하도록 수정한다.
@oungsi2000 oungsi2000 self-assigned this Aug 6, 2026
@oungsi2000 oungsi2000 added the feature 새로운 기능 추가 label Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2386ad52-c41d-4317-9c2a-61879a151ec0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

채팅 상태 모델과 ChatViewModel을 추가했습니다. 채팅 진입과 새 대화를 처리합니다. 메시지를 스트리밍으로 전송하고 Start, Delta, Action, Done, Error 이벤트를 반영합니다. 타자 표시 Flow와 ViewModel 및 Flow 테스트를 추가했습니다.

Changes

채팅 흐름

Layer / File(s) Summary
채팅 상태와 진입 동작
feature/chat/src/main/java/com/kikidan/chat/model/*, feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt, feature/chat/src/main/AndroidManifest.xml, feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt
ChatState, StreamingChatState, ChatSideEffect를 추가했습니다. 채팅 진입 데이터와 대화 상세 조회, 입력 길이 제한, 추천 문구 전송, 새 대화 초기화를 구현했습니다. 관련 상태와 오류 처리를 테스트했습니다.
메시지 스트리밍과 타자 표시
feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt, feature/chat/src/main/java/com/kikidan/chat/TypewriterFlow.kt, feature/chat/src/test/java/com/kikidan/chat/FakeChatRepository.kt, feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt, feature/chat/src/test/java/com/kikidan/chat/TypewriterFlowTest.kt
사용자 placeholder 메시지와 단일 전송 가드를 추가했습니다. 스트리밍 이벤트로 대화 ID, 메시지 ID, quota, assistant 메시지, action을 갱신합니다. typewriter가 청크를 누적해 표시 텍스트를 방출합니다. 정상 처리, 오류, 취소, 중복 전송, 예외 전파를 테스트했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 ChatViewModel과 TypewriterFlow 구현이라는 주요 변경을 명확히 요약합니다.
Description check ✅ Passed 관련 이슈, 작업 내용, 구현 상세를 포함하며 선택 항목을 제외한 주요 섹션을 작성했습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@oungsi2000
oungsi2000 changed the base branch from main to feat/76-chat-data-layer August 6, 2026 16:04
@oungsi2000

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt (2)

105-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

[P3] 변수 섀도잉과 메서드명 오타를 정리하십시오.

Line 108의 지역 변수 result가 Line 105 람다 파라미터 result를 가립니다. 이름을 분리하십시오. 또한 onSteamingDoneonStreamingDone의 오타입니다(Line 138, 179).

♻️ 제안 수정
                             is ChatStreamEvent.Start -> {
-                                val result = onStreamingStart(placeholder, event)
-                                streamConversationId = result.first
-                                assistantMessageId = result.second
+                                val (startedConversationId, startedAssistantId) =
+                                    onStreamingStart(placeholder, event)
+                                streamConversationId = startedConversationId
+                                assistantMessageId = startedAssistantId
                             }

Also applies to: 179-179

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt` around lines
105 - 110, Rename the local `result` inside the `ChatStreamEvent.Start` branch
of the `.transform` lambda to avoid shadowing the lambda parameter, and update
all references to it. Correct the misspelled `onSteamingDone` method references
to `onStreamingDone` at both call sites.

33-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

[P2] conversationId와 작성 중 input의 SavedStateHandle 적용을 검토하십시오.

프로세스가 종료되면 현재 대화 식별자와 작성 중 입력값이 사라집니다. 두 값은 복원 대상입니다. SavedStateHandleconversationId를 받아 초기 상태에 반영하고, input 저장도 함께 검토하십시오.

이 지적은 코딩 가이드 기준입니다. As per path instructions: "상태 보존이 필요한 값은 SavedStateHandle 사용을 검토하세요."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt` around lines 33
- 38, Update the ChatViewModel state initialization and load flow to use
SavedStateHandle for the restorable conversationId, applying its saved value to
the initial ChatState and persisting updates when load receives a new ID. Also
persist and restore the in-progress input value through the same handle, keeping
the existing state updates intact.

Source: Path instructions

feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt (1)

344-385: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

[P2] onSendClick 경로 테스트가 없습니다.

전송 테스트는 모두 onSuggestionClick을 사용합니다. onSendClick의 입력 초기화 순서와 공백 입력 처리는 검증되지 않습니다. 다음 두 케이스를 추가하십시오.

  • 스트리밍 중 onSendClick 호출 후 input이 유지되는지 확인합니다.
  • 공백만 입력한 상태에서 onSendClick 호출 시 sendCallCount가 0인지 확인합니다.

테스트 코드를 작성해 드릴까요?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt` around
lines 344 - 385, Add tests covering the onSendClick path in ChatViewModelTest:
verify that invoking onSendClick during streaming preserves the current input,
and verify that onSendClick with whitespace-only input does not call the
repository, keeping sendCallCount at zero. Reuse the existing viewModel,
FakeChatRepository, and vm.test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt`:
- Around line 143-152: Update localUserMessage creation to use a pending status
instead of MessageStatus.COMPLETED, then transition it to COMPLETED when
ChatStreamEvent.Start is received. In the Throwable catch block, mark the
optimistic message as failed while preserving the existing streaming state reset
and error side effect so the resend UI can appear.
- Around line 65-70: Update onSendClick and send so input normalization and
clearing occur inside send: trim the content once on entry, return without
clearing or sending when it is blank or while streaming, and use the same
trimmed value for the placeholder and sendChatMessage. Remove the premature
state.copy(input = "") from onSendClick.
- Around line 74-84: Update startNewConversation and the send() streaming flow
so an in-progress response from the previous conversation cannot update the new
conversation state: either cancel the active streaming job when starting a new
conversation, or validate the captured stream/conversation identifiers before
applying terminal events such as Done. Ensure stale events are ignored after the
conversation is reset.

In `@feature/chat/src/main/java/com/kikidan/chat/TypewriterFlow.kt`:
- Around line 8-11: TypewriterFlow의 KDoc 설명에서 잘못된 따옴표를 제거하여 `전체 텍스"를`을 올바른 문구로
수정하십시오.

In `@feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt`:
- Around line 150-153: Update the failure message in the streamingText
monotonicity assertion to include the current streaming text value separately
from the previous value; replace the duplicated chatState interpolation while
preserving the existing startsWith check and assertion behavior.

---

Nitpick comments:
In `@feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt`:
- Around line 105-110: Rename the local `result` inside the
`ChatStreamEvent.Start` branch of the `.transform` lambda to avoid shadowing the
lambda parameter, and update all references to it. Correct the misspelled
`onSteamingDone` method references to `onStreamingDone` at both call sites.
- Around line 33-38: Update the ChatViewModel state initialization and load flow
to use SavedStateHandle for the restorable conversationId, applying its saved
value to the initial ChatState and persisting updates when load receives a new
ID. Also persist and restore the in-progress input value through the same
handle, keeping the existing state updates intact.

In `@feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt`:
- Around line 344-385: Add tests covering the onSendClick path in
ChatViewModelTest: verify that invoking onSendClick during streaming preserves
the current input, and verify that onSendClick with whitespace-only input does
not call the repository, keeping sendCallCount at zero. Reuse the existing
viewModel, FakeChatRepository, and vm.test setup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c738278e-b79f-4cd8-929b-3607efcf582a

📥 Commits

Reviewing files that changed from the base of the PR and between 442322c and ce69c36.

⛔ Files ignored due to path filters (3)
  • app/build.gradle.kts is excluded by !**/*.gradle.kts
  • feature/chat/build.gradle.kts is excluded by !**/*.gradle.kts
  • settings.gradle.kts is excluded by !**/*.gradle.kts
📒 Files selected for processing (8)
  • feature/chat/src/main/AndroidManifest.xml
  • feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt
  • feature/chat/src/main/java/com/kikidan/chat/TypewriterFlow.kt
  • feature/chat/src/main/java/com/kikidan/chat/model/ChatSideEffect.kt
  • feature/chat/src/main/java/com/kikidan/chat/model/ChatState.kt
  • feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt
  • feature/chat/src/test/java/com/kikidan/chat/FakeChatRepository.kt
  • feature/chat/src/test/java/com/kikidan/chat/TypewriterFlowTest.kt

Comment on lines +65 to +70
fun onSendClick() =
intent {
val content = state.input
reduce { state.copy(input = "") }
send(content)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

[P1] 전송 실패 시 입력값이 유실되고, 공백 입력이 그대로 전송됩니다.

onSendClicksend 호출 전에 input을 비웁니다. send의 가드(Line 88)가 스트리밍 중 즉시 return하면 사용자가 작성한 텍스트가 사라집니다.

또한 공백 검사가 없습니다. 공백만 입력해도 서버로 전송되고 빈 placeholder가 messages에 추가됩니다.

추가로 placeholder는 content.trim()(Line 91)을 쓰지만 sendChatMessage에는 원본 content(Line 104)를 전달합니다. 화면 표시 값과 서버 전송 값이 달라집니다. send 진입 시 한 번 trim하여 두 경로에 같은 값을 쓰십시오.

🐛 제안 수정
         fun onSendClick() =
             intent {
-                val content = state.input
-                reduce { state.copy(input = "") }
-                send(content)
+                send(state.input)
             }

send 내부에서 정규화와 입력 초기화를 함께 처리합니다.

         private suspend fun Syntax<ChatState, ChatSideEffect>.send(content: String) {
             if (state.streamingChatState !is StreamingChatState.Idle) return
+            val trimmed = content.trim()
+            if (trimmed.isBlank()) return
 
             val conversationId = state.conversationId
-            val placeholder = localUserMessage(content.trim())
+            val placeholder = localUserMessage(trimmed)
             reduce {
                 state.copy(
                     messages = state.messages + placeholder,
                     streamingChatState = StreamingChatState.Thinking,
+                    input = "",
                 )
             }

그리고 Line 104의 전송도 정규화된 값을 사용합니다.

-                sendChatMessage(conversationId, content)
+                sendChatMessage(conversationId, trimmed)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun onSendClick() =
intent {
val content = state.input
reduce { state.copy(input = "") }
send(content)
}
fun onSendClick() =
intent {
send(state.input)
}
Suggested change
fun onSendClick() =
intent {
val content = state.input
reduce { state.copy(input = "") }
send(content)
}
private suspend fun Syntax<ChatState, ChatSideEffect>.send(content: String) {
if (state.streamingChatState !is StreamingChatState.Idle) return
val trimmed = content.trim()
if (trimmed.isBlank()) return
val conversationId = state.conversationId
val placeholder = localUserMessage(trimmed)
reduce {
state.copy(
messages = state.messages + placeholder,
streamingChatState = StreamingChatState.Thinking,
input = "",
)
}
Suggested change
fun onSendClick() =
intent {
val content = state.input
reduce { state.copy(input = "") }
send(content)
}
sendChatMessage(conversationId, trimmed)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt` around lines 65
- 70, Update onSendClick and send so input normalization and clearing occur
inside send: trim the content once on entry, return without clearing or sending
when it is blank or while streaming, and use the same trimmed value for the
placeholder and sendChatMessage. Remove the premature state.copy(input = "")
from onSendClick.

Comment on lines +74 to +84
fun startNewConversation() =
intent {
reduce {
state.copy(
conversationId = null,
messages = emptyList(),
streamingChatState = StreamingChatState.Idle,
input = "",
)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Orbit MVI container intent execution sequential or parallel by default

💡 Result:

In Orbit MVI (Kotlin), intents are processed sequentially per container by default (not parallel), because each container uses an “event loop” coroutine that receives intent jobs from a channel and then launches each intent coroutine in turn; the next intent is taken from the channel only after the current loop iteration proceeds. The default threading model is configured via RealSettings: intents are launched using intentLaunchingDispatcher (default Dispatchers.Unconfined) and the container’s event-loop runs on eventLoopDispatcher (default Dispatchers.Default) [1][2]. What this means for “sequential vs parallel”: 1) Ordering of intents: Orbit processes intent requests through an internal dispatchChannel + event-loop, which provides a queue/serialized intake per container [1]. 2) Parallel decomposition within a single intent: you can run multiple sub-blocks concurrently by using coroutineScope { launch { … } }. Orbit provides subIntent “for parallel decomposition or subdivision of a larger intent into smaller parts” [3][4]. Those launched coroutines run concurrently as normal Kotlin coroutines within that one intent’s scope. 3) Parallelism across containers: dispatcher settings are per-container instances (factories invoked once per container), so different containers can run intents independently; also Orbit.configureDefaults notes serialization can be limited per-container depending on how you set intentLaunchingDispatcher [4]. Defaults you can rely on: - Calls to Container.intent do not block the caller; Orbit offloads the work to an event-loop style background coroutine, and long-running/blocking work inside intents can block that event-loop [4]. - sideEffectMode defaults to FAN_OUT, but that pertains to side-effect delivery rather than intent execution ordering [2]. To make intent execution parallel, you generally wouldn’t change “default intent queueing” directly; instead you’d keep intents serialized but fetch independent data in parallel within a single intent using coroutineScope/async and then reduce once (or use subIntent for decomposing/launching concurrent collectors inside the intent) [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate ChatViewModel and related search results =="
fd -a 'ChatViewModel\.kt$' . | sed 's#^\./##'
echo

echo "== Candidate file outline =="
file="$(fd 'ChatViewModel.kt$' . | head -n 1 || true)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  ast-grep outline "$file" --view compact || true
  echo
  echo "== Relevant ChatViewModel sections =="
  sed -n '1,260p' "$file" | cat -n
fi

echo
echo "== Search streaming/state handling symbols =="
rg -n "streamConversationId|streamingChatState|startNewConversation|onSteamingDone|streamConversation|cancel|Job|CoroutineScope|container|Orbit" . --glob '*.kt' -C 2

echo
echo "== Build config orbit versions =="
rg -n "orbit|kotlin-coroutines|coroutines" -S . --glob 'build.gradle*' --glob '*.gradle.kts' --glob 'libs.versions.toml' -C 1 || true

Repository: YAPP-Github/28th-App-Team-2-Android

Length of output: 50391


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ChatViewModel tests around concurrent intent/startNewConversation =="
sed -n '350,430p' feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt | cat -n

echo
echo "== ChatViewModel tests around send concurrency =="
sed -n '184,370p' feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt | cat -n

echo
echo "== SendChatMessage remote repo flow implementation outline =="
sed -n '1,240p' core/data/src/main/java/com/kikidan/data/repository/ChatRepositoryImpl.kt | cat -n
sed -n '1,260p' core/data-remote/src/main/java/com/kikidan/data_remote/datasource/ChatRemoteDataSourceImpl.kt | cat -n 2>/dev/null || true

echo
echo "== Gradle orbit/coroutines declarations =="
rg -n "orbit|kotlinx-coroutines|coroutines" -S . --glob 'build.gradle*' --glob '*.gradle.kts' --glob 'libs.versions.toml' --glob 'gradle/libs.versions.toml' -C 1

Repository: YAPP-Github/28th-App-Team-2-Android

Length of output: 20172


[P2] 새 대화 시작 시 진행 중인 응답 흐름도 함께 종료해야 합니다.

startNewConversation은 상태만 초기화하지만, send()streamStreamingIdassistantMessageId를 캡처한 채 Flow를 계속 수집합니다. 이전에 선택한 대화의 Done 이벤트가 새 conversationId/답변으로 덮어씌워질 수 있습니다. 새 대화 요청 시 프록시 상태가 실제 대화 식별자와 여전히 일치하는지 확인한 뒤에만 최종 적용하거나, 새 대화 요청으로 진행 중인 job을 취소하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt` around lines 74
- 84, Update startNewConversation and the send() streaming flow so an
in-progress response from the previous conversation cannot update the new
conversation state: either cancel the active streaming job when starting a new
conversation, or validate the captured stream/conversation identifiers before
applying terminal events such as Done. Ensure stale events are ignored after the
conversation is reset.

Comment on lines +143 to +152
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
reduce {
state.copy(
streamingChatState = StreamingChatState.Idle,
)
}
postSideEffect(ChatSideEffect.Error(e))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

[P2] 전송 실패 시 낙관적 사용자 메시지가 성공 상태로 남습니다.

localUserMessage(Line 210)는 서버 확인 전에 MessageStatus.COMPLETED를 설정합니다. 스트림이 실패하면 이 메시지는 계속 성공 상태로 표시됩니다.

낙관적 메시지는 전송 대기 상태로 생성하고, ChatStreamEvent.Start 수신 시 COMPLETED로 갱신하십시오. 실패 시에는 catch 블록에서 실패 상태로 표시해 재전송 UI를 제공할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt` around lines
143 - 152, Update localUserMessage creation to use a pending status instead of
MessageStatus.COMPLETED, then transition it to COMPLETED when
ChatStreamEvent.Start is received. In the Throwable catch block, mark the
optimistic message as failed while preserving the existing streaming state reset
and error side effect so the resend UI can appear.

Comment on lines +8 to +11
/**
* 도착 속도(네트워크)와 표시 속도(화면)를 분리한다.
* 업스트림 청크를 버퍼에 쌓고 tick마다 조금씩 잘라 지금까지 보여줄 전체 텍스"를 방출한다.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

[P3] KDoc 오타를 수정하십시오.

전체 텍스"를에 잘못된 따옴표가 있습니다.

✏️ 제안 수정
- * 업스트림 청크를 버퍼에 쌓고 tick마다 조금씩 잘라 지금까지 보여줄 전체 텍스"를 방출한다.
+ * 업스트림 청크를 버퍼에 쌓고 tick마다 조금씩 잘라 지금까지 보여줄 전체 텍스트를 방출한다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 도착 속도(네트워크)와 표시 속도(화면)를 분리한다.
* 업스트림 청크를 버퍼에 쌓고 tick마다 조금씩 잘라 지금까지 보여줄 전체 텍스" 방출한다.
*/
/**
* 도착 속도(네트워크)와 표시 속도(화면)를 분리한다.
* 업스트림 청크를 버퍼에 쌓고 tick마다 조금씩 잘라 지금까지 보여줄 전체 텍스트를 방출한다.
*/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/chat/src/main/java/com/kikidan/chat/TypewriterFlow.kt` around lines 8
- 11, TypewriterFlow의 KDoc 설명에서 잘못된 따옴표를 제거하여 `전체 텍스"를`을 올바른 문구로 수정하십시오.

Comment on lines +150 to +153
assertTrue(
"streamingText 단조 증가 실패: '$chatState' → '$chatState'",
currentChatState.streamingText.startsWith(chatState.streamingText),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

[P3] 단조 증가 실패 메시지에 현재 값이 빠졌습니다.

$chatState가 두 번 사용되어 이전 값만 출력됩니다.

💚 제안 수정
                     assertTrue(
-                        "streamingText 단조 증가 실패: '$chatState' → '$chatState'",
+                        "streamingText 단조 증가 실패: '${chatState.streamingText}' → '${currentChatState.streamingText}'",
                         currentChatState.streamingText.startsWith(chatState.streamingText),
                     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assertTrue(
"streamingText 단조 증가 실패: '$chatState' → '$chatState'",
currentChatState.streamingText.startsWith(chatState.streamingText),
)
assertTrue(
"streamingText 단조 증가 실패: '${chatState.streamingText}' → '${currentChatState.streamingText}'",
currentChatState.streamingText.startsWith(chatState.streamingText),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt` around
lines 150 - 153, Update the failure message in the streamingText monotonicity
assertion to include the current streaming text value separately from the
previous value; replace the duplicated chatState interpolation while preserving
the existing startsWith check and assertion behavior.

todakun.feature 플러그인을 사용하여 개별 모듈에서 반복되는 빌드 설정을 통합 관리한다.
이로 인해 중복되는 Android SDK 버전 설정, 컴파일 옵션 및 Hilt, Orbit, Compose 관련 공통 의존성 선언을 삭제하여 빌드 스크립트를 간소화한다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant