[qa/#332] 스프린트 qa 반영 - #333
Conversation
Walkthrough댓글 제출 성공 이벤트를 ViewModel에서 화면으로 전달하도록 변경했습니다. 제출 후 입력 상태와 키보드를 정리하고 목록을 하단으로 스크롤합니다. 답글 스크롤과 댓글 텍스트 접기 계산도 조정했습니다. 앱 보호 규칙과 버전을 갱신했습니다. Changes댓글 및 답글 흐름
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CommonAnswerScreen
participant CommonOtherAnswerViewModel
participant commentSubmissionSuccess
participant LazyListState
CommonAnswerScreen->>CommonOtherAnswerViewModel: 댓글 제출
CommonOtherAnswerViewModel->>commentSubmissionSuccess: 성공 이벤트 방출
commentSubmissionSuccess->>CommonAnswerScreen: Unit 수집
CommonAnswerScreen->>LazyListState: 다음 프레임 후 하단 스크롤
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
app/proguard-rules.pro (1)
131-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win중복된 ProGuard 블록을 제거하세요.
app/proguard-rules.pro의 Line 131-163은 같은 파일의 Line 97-129와 동일합니다. 따라서 이 변경은 release shrinker에 새로운 규칙을 추가하지 않습니다. 새 규칙이 목적이면 누락된 규칙만 남기고, 그렇지 않으면 Line 131-163을 삭제하세요. 수정 후 release minification 빌드도 확인하세요.🤖 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 `@app/proguard-rules.pro` around lines 131 - 163, Remove the duplicated ProGuard rule block corresponding to the Kotlin Serialization through protectNavigation entries, keeping the existing earlier block unchanged. If any entries were intended to be new, retain only rules absent from the earlier block, then verify the release minification build.
🤖 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
`@app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonReplyItem.kt`:
- Around line 161-180: CommonReplyItem의 이진 탐색에서 candidateEnd가 UTF-16 surrogate
pair 중간을 가리키지 않도록 후보 인덱스를 유효한 code point 경계로 보정하거나 code point 기준으로 계산하세요.
substring(0, candidateEnd)가 항상 완전한 문자를 포함하도록 유지하고, emoji가 포함된 댓글의 접힘 결과를 검증하는
테스트도 추가하세요.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerScreen.kt`:
- Around line 275-279: Update the edit-comment flow around onEditCommentComplete
so commentText, keyboard visibility, and focus are reset only after the
asynchronous save succeeds and editingComment is cleared. Preserve the current
input and focus when saving fails and editingComment remains set.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt`:
- Around line 486-510: In the MoreOptionTarget.Comment branch of the surrounding
deletion handling, remove the branch-local loadCommonAnswer() call and retain
the shared call after the when block so comment deletion triggers the detail
request only once. Keep the reply-specific loadCommentReplies(commentId)
behavior unchanged.
- Around line 242-250: Update CommonAnswerViewModel.onCommentComplete to track
comment submission state, preventing repeated uploadComment calls while a
request is in progress, following the existing onReplyComplete/isReplySubmitting
pattern. Expose the state to CommonAnswerScreen and pass it to CommentInputBar
so the completion button is disabled during submission, while preserving the
existing success reload behavior.
- Around line 313-318: 일반 댓글 수정 성공 후 답글 바텀시트가 닫혀 있어도 화면의 댓글 목록이 즉시 갱신되도록 수정하세요.
CommonAnswerViewModel의 editingComment 처리에서 updateCommonQuestComment 성공 시
loadCommonAnswer()를 호출하거나 수정된 댓글을 uiState.comments에 반영하고, 기존 답글 바텀시트가 열린 경우의
loadCommentReplies(editingComment.target.id) 동작은 유지하세요.
---
Nitpick comments:
In `@app/proguard-rules.pro`:
- Around line 131-163: Remove the duplicated ProGuard rule block corresponding
to the Kotlin Serialization through protectNavigation entries, keeping the
existing earlier block unchanged. If any entries were intended to be new, retain
only rules absent from the earlier block, then verify the release minification
build.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c5f3b433-1087-4215-bb77-2e722adcb85f
📒 Files selected for processing (6)
app/proguard-rules.proapp/src/main/java/com/byeboo/app/presentation/quest/component/bottomsheet/ReplyBottomSheet.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonReplyItem.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.ktgradle/libs.versions.toml
| while (low <= high) { | ||
| val candidateEnd = (low + high) / 2 | ||
| val candidate = | ||
| reply.content | ||
| .substring(0, candidateEnd) | ||
| .trimEnd() + "... 더보기" | ||
| val candidateLayout = | ||
| textMeasurer.measure( | ||
| text = candidate, | ||
| style = bodyTextStyle, | ||
| overflow = TextOverflow.Clip, | ||
| maxLines = 5, | ||
| constraints = Constraints(maxWidth = result.size.width), | ||
| ) | ||
|
|
||
| if (!candidateLayout.hasVisualOverflow) { | ||
| bestEnd = candidateEnd | ||
| low = candidateEnd + 1 | ||
| } else { | ||
| high = candidateEnd - 1 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | sed -n '/CommonReplyItem.kt/p'
echo "== file outline =="
ast-grep outline app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonReplyItem.kt --view compact || true
echo "== relevant lines =="
sed -n '130,205p' app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonReplyItem.kt
echo "== usages test for emoji surrogate boundary probe =="
python3 - <<'PY'
from pathlib import Path
import re
p=Path('app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonReplyItem.kt')
text=p.read_text()
print('binary_search_candidateEnd_found', '(low + high) / 2' in text)
print('substring_candidate_end_found', 'substring(0, candidateEnd)' in text)
print('trimEnd_plus_more_found', '.trimEnd() + "... 더보기"' in text)
PYRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 3813
🌐 Web query:
Android Jetpack Compose TextLayoutResult result.getLineEnd lineEnd index UTF-16 code unit character
💡 Result:
TextLayoutResult.getLineEnd(lineIndex, visibleEnd) returns an exclusive end offset (an index into the original text) “a position in text after the last character in the given line” [1]. About “UTF-16 code unit character”: the Compose TextLayoutResult APIs are defined in terms of text offsets, and the returned offsets are used as indices for substring/end positions (i.e., they correspond to offsets into the string as consumed by Kotlin/Compose text APIs) [1][2]. In Kotlin/Java, String indexing/substring boundaries are UTF-16 code unit indices, so the integer offsets returned by getLineEnd are UTF-16 code unit offsets when applied to the original String [2][1]. Additional details from the official docs: - visibleEnd=false (default) returns the “logical line end” including trailing whitespaces/linefeed characters and, for ellipsis, counts ellipsized characters as visible [1]. - visibleEnd=true returns a more strictly “visible” end that does not count trailing whitespaces/linefeed characters (and ellipsized characters are treated as invisible for the purpose of determining the end) [1]. Practical takeaway: - Treat the result as a String UTF-16 code unit index (exclusive end). For example: text.substring(textLayoutResult.getLineStart(i), textLayoutResult.getLineEnd(i, visibleEnd = true)) [1][2].
Citations:
- 1: https://developer.android.com/reference/kotlin/androidx/compose/ui/text/TextLayoutResult
- 2: https://proandroiddev.com/detect-text-overflow-in-jetpack-compose-56c0b83da5a5
UTF-16 surrogate pair 중간에서 접힌 댓글 텍스트를 자르지 마세요.
candidateEnd와 reply.content.length 모두 Kotlin String의 UTF-16 code unit 기준 인덱스입니다. 이진 탐색 중간 지점이 emoji surrogate pair 사이일 때 substring(0, candidateEnd)는 잘린 surrogate를 포함하므로 접힌 댓글에 대체 문자가 표시됩니다.
후보 midpoint에서 surrogate pair 경계를 보정하거나 code point 기준 자르기 방식을 사용하세요. emoji를 포함한 접힘 계산 테스트도 함께 추가하세요.
🤖 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
`@app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonReplyItem.kt`
around lines 161 - 180, CommonReplyItem의 이진 탐색에서 candidateEnd가 UTF-16 surrogate
pair 중간을 가리키지 않도록 후보 인덱스를 유효한 code point 경계로 보정하거나 code point 기준으로 계산하세요.
substring(0, candidateEnd)가 항상 완전한 문자를 포함하도록 유지하고, emoji가 포함된 댓글의 접힘 결과를 검증하는
테스트도 추가하세요.
| if (uiState.editingComment?.target is MoreOptionTarget.Comment && !uiState.showReplyBottomSheet) { | ||
| onEditCommentComplete(commentText) | ||
| commentText = "" | ||
| keyboardController?.hide() | ||
| focusManager.clearFocus() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
수정 성공 뒤에만 입력 상태를 초기화하세요.
onEditCommentComplete는 비동기로 저장을 시작합니다. 저장이 실패하면 ViewModel은 editingComment를 유지합니다. 그러나 이 코드는 즉시 commentText를 비우고 키보드를 숨깁니다. 사용자는 수정한 내용을 잃고 다시 입력해야 합니다.
저장 성공으로 editingComment가 해제된 뒤에만 입력값, 포커스, 키보드를 초기화하세요. 실패 시에는 현재 입력값과 포커스를 유지하세요.
🤖 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
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerScreen.kt`
around lines 275 - 279, Update the edit-comment flow around
onEditCommentComplete so commentText, keyboard visibility, and focus are reset
only after the asynchronous save succeeds and editingComment is cleared.
Preserve the current input and focus when saving fails and editingComment
remains set.
| fun onCommentComplete(content: String) { | ||
| viewModelScope.launch { | ||
| commonQuestRepository | ||
| .uploadComment( | ||
| content = content, | ||
| targetId = answerId, | ||
| ).onSuccess { | ||
| loadCommonAnswer(notifyCommentSubmissionSuccess = true) | ||
| }.onFailure { exception -> |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
input_file="$(fd -t f -a '^CommentInputBar\.kt$' app/src/main/java | head -n 1)"
if [[ -n "${input_file}" ]]; then
ast-grep outline "${input_file}" --items all
fi
rg -nP --type=kotlin -C 6 \
'\bfun\s+CommentInputBar\s*\(|\bisCompleteEnabled\b|\bonCompleteClick\b' \
app/src/main/java
rg -nP --type=kotlin -C 6 \
'\bfun\s+onCommentComplete\s*\(|\buploadComment\s*\(' \
app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 50387
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== CommonAnswerViewModel comment submission-related section =="
rg -nC 12 '\bstate\b|\bCommonAnswerState\b|uploadComment|onCommentComplete|isReplySubmitting|isReplying|ReplyBottomSheet\(' \
app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt \
| head -n 240
echo
echo "== CommonAnswerScreen CommentInputBar call =="
sed -n '120,290p' app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerScreen.kt | cat -n | sed 's/^/Screen: /'
echo
echo "== ReplyBottomSheet submit guard =="
sed -n '136,156p' app/src/main/java/com/byeboo/app/presentation/quest/component/bottomsheet/ReplyBottomSheet.kt | cat -n | sed 's/^/BottomSheet: /'Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 20838
댓글 제출 중 중복 요청을 차단하세요.
CommonAnswerScreen의 isCompleteEnabled = commentText.isNotEmpty()는 요청 중 비활성화를 하지 않습니다. CommonAnswerViewModel.onCommentComplete()도 onReplyComplete()처럼 isReplySubmitting 같은 상태를 사용해 보호하지 않습니다. 사용자의 빠른 입력 또는 반복 클릭으로 uploadComment()가 여러 번 실행되어 중복 댓글이 생성될 수 있습니다. 댓글 제출 중 상태를 함께 관리하고, CommentInputBar의 완료 버튼 비활성 상태로 전달하세요.
🤖 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
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt`
around lines 242 - 250, Update CommonAnswerViewModel.onCommentComplete to track
comment submission state, preventing repeated uploadComment calls while a
request is in progress, following the existing onReplyComplete/isReplySubmitting
pattern. Expose the state to CommonAnswerScreen and pass it to CommentInputBar
so the completion button is disabled during submission, while preserving the
existing success reload behavior.
| when (editingComment.target) { | ||
| is MoreOptionTarget.Comment -> { | ||
| if (_uiState.value.showReplyBottomSheet) { | ||
| loadCommentReplies(editingComment.target.id) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline \
app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt \
--items all
rg -nP --type=kotlin -C 8 \
'\b(fun|override\s+fun)\s+updateCommonQuestComment\s*\(|\brefreshEvent\b|(?:try)?emit\s*\(' \
app/src/main/javaRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 50388
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== CommonAnswerViewModel outline =="
ast-grep outline \
app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt \
--view expanded || true
echo "== CommonAnswerViewModel relevant sections =="
wc -l app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt
sed -n '1,120p' app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt
sed -n '260,360p' app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt
sed -n '360,460p' app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt
echo "== updateCommonQuestComment implementations/calls =="
rg -n --type=kotlin \
-C 4 \
'updateCommonQuestComment|refreshEvent|refetchCommonQuests|loadCommonAnswer|comments\b' \
app/src/main/java/com/byeboo/app \
--glob '!**/build/**' |\
rg -C 4 'app/src/main/java/(com/byeboo/app/(data|domain)/.*|.*ViewModel|.*Repository\b)' || true
echo "== CommonAnswerRepository candidates =="
fd -i 'Common.*Repository|QuestCommon.*Repository|Common.*Answer.*Repository' app/src/main/java -t fRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 50387
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== focused repository/implementation search =="
rg -n --type=kotlin \
-C 8 \
'interface\s+\w*Repository|class\s+\w*Implementation|override\s+fun\s+updateCommonQuestComment|_?\brefreshEvent\b|StateFlow|MutableStateFlow' \
app/src/main/java/com/byeboo/app \
--glob '!**/build/**'Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 50388
일반 댓글 수정 뒤 목록 갱신 경로를 확인하세요.
QuestCommonRepositoryImpl.updateCommonQuestComment는 성공 시 _refreshEvent.emit(Unit)을 호출하지만, CommonAnswerViewModel의 collect { loadCommonAnswer() }는 이 이벤트가 화면의 loadCommentReplies(...) 수행 이후에야 상세 재조회를 수행합니다. 답글 바텀시트가 닫힌 상태에서 댓글을 수정한 경우, 화면은 목록 상의 해당 댓글 내용을 직접 갱신하지도 않고 즉시 loadCommonAnswer()를 호출하지도 않습니다. 성공 뒤에 상세를 다시 조회하거나 수정된 댓글을 uiState.comments에 반영하세요.
🤖 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
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt`
around lines 313 - 318, 일반 댓글 수정 성공 후 답글 바텀시트가 닫혀 있어도 화면의 댓글 목록이 즉시 갱신되도록 수정하세요.
CommonAnswerViewModel의 editingComment 처리에서 updateCommonQuestComment 성공 시
loadCommonAnswer()를 호출하거나 수정된 댓글을 uiState.comments에 반영하고, 기존 답글 바텀시트가 열린 경우의
loadCommentReplies(editingComment.target.id) 동작은 유지하세요.
| when (target) { | ||
| is MoreOptionTarget.Comment -> { | ||
| loadCommonAnswer() | ||
|
|
||
| if (uiState.value.selectedComment?.replyId == target.id) { | ||
| _uiState.update { | ||
| it.copy( | ||
| showReplyBottomSheet = false, | ||
| selectedComment = null, | ||
| selectedReplies = persistentListOf(), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| is MoreOptionTarget.Reply -> { | ||
| uiState.value.selectedComment?.replyId?.let { commentId -> | ||
| loadCommentReplies(commentId) | ||
| is MoreOptionTarget.Reply -> { | ||
| uiState.value.selectedComment?.replyId?.let { commentId -> | ||
| loadCommentReplies(commentId) | ||
| } | ||
| loadCommonAnswer() | ||
| } | ||
| loadCommonAnswer() | ||
| } | ||
|
|
||
| else -> Unit | ||
| else -> Unit | ||
| } | ||
| loadCommonAnswer() |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
댓글 삭제 뒤 상세 조회를 한 번만 실행하세요.
MoreOptionTarget.Comment 분기는 loadCommonAnswer()를 실행합니다. 분기 뒤의 공통 코드도 같은 함수를 실행합니다. 삭제 한 번에 상세 요청과 실패 스낵바가 중복될 수 있습니다.
수정 예시
is MoreOptionTarget.Comment -> {
- loadCommonAnswer()
-
if (uiState.value.selectedComment?.replyId == target.id) {
_uiState.update {
it.copy(📝 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.
| when (target) { | |
| is MoreOptionTarget.Comment -> { | |
| loadCommonAnswer() | |
| if (uiState.value.selectedComment?.replyId == target.id) { | |
| _uiState.update { | |
| it.copy( | |
| showReplyBottomSheet = false, | |
| selectedComment = null, | |
| selectedReplies = persistentListOf(), | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| is MoreOptionTarget.Reply -> { | |
| uiState.value.selectedComment?.replyId?.let { commentId -> | |
| loadCommentReplies(commentId) | |
| is MoreOptionTarget.Reply -> { | |
| uiState.value.selectedComment?.replyId?.let { commentId -> | |
| loadCommentReplies(commentId) | |
| } | |
| loadCommonAnswer() | |
| } | |
| loadCommonAnswer() | |
| } | |
| else -> Unit | |
| else -> Unit | |
| } | |
| loadCommonAnswer() | |
| when (target) { | |
| is MoreOptionTarget.Comment -> { | |
| if (uiState.value.selectedComment?.replyId == target.id) { | |
| _uiState.update { | |
| it.copy( | |
| showReplyBottomSheet = false, | |
| selectedComment = null, | |
| selectedReplies = persistentListOf(), | |
| ) | |
| } | |
| } | |
| } | |
| is MoreOptionTarget.Reply -> { | |
| uiState.value.selectedComment?.replyId?.let { commentId -> | |
| loadCommentReplies(commentId) | |
| } | |
| loadCommonAnswer() | |
| } | |
| else -> Unit | |
| } | |
| loadCommonAnswer() |
🤖 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
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/CommonAnswerViewModel.kt`
around lines 486 - 510, In the MoreOptionTarget.Comment branch of the
surrounding deletion handling, remove the branch-local loadCommonAnswer() call
and retain the shared call after the when block so comment deletion triggers the
detail request only once. Keep the reply-specific loadCommentReplies(commentId)
behavior unchanged.
Related issue 🛠
Work Description 📝
Screenshot 📸
Uncompleted Tasks 😅
PR Point 📌
트러블 슈팅 💥
Summary by CodeRabbit
개선 사항
릴리스