[chore/#315] 공통 답장 컴포넌트 변경#321
Conversation
Walkthrough공통 퀘스트 답변에 하트·댓글 집계와 좋아요 상태를 추가했습니다. API DTO·매퍼·도메인·프레젠테이션 모델, Compose UI(ReactionCountButton), 화면과 뷰모델 연결 및 Retrofit 경로를 v2로 갱신했습니다. Changes하트/댓글 반응 기능
Estimated code review effort 🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 9
🤖 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/button/ReactionCountButton.kt`:
- Around line 27-29: The ReactionCountButton component currently hides missing
wiring by giving onCommentClick a no-op default and answerId a fake 0L default;
change the function signature for ReactionCountButton to require onCommentClick:
(Long) -> Unit and answerId: Long with no defaults (i.e., make them mandatory
parameters), then update all call sites to pass a real click handler and a valid
answerId (or propagate the required params) so missing connections are caught at
compile time rather than silently ignored.
- Around line 55-69: ReactionCount currently exposes only an icon (Image with
contentDescription=null) and a Text count, so screen readers read only the
number; update ReactionCount to provide an accessible label and role by adding
semantics on the clickable Row (the same composable that uses
noRippleClickable/clickable) — populate Modifier.semantics { contentDescription
= "${label} $count" ; this.role = Role.Button } where label differentiates heart
vs comment (use an input param or resource string), and keep Image
contentDescription=null to avoid duplicate announcements; ensure the semantic
contentDescription is localized (use string resources) and includes the count so
assistive tech reads e.g. "Like 3" or "Comment 2".
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/card/MyAnswerItem.kt`:
- Around line 29-30: The heart callback currently has signature onHeartClick: ()
-> Unit so parent cannot know which answer was toggled; change the prop
signature in MyAnswerItem (and any usages) to onHeartClick: (Long) -> Unit
(matching onCommentClick), and update all invocations inside the component
(where the heart button is clicked) to call onHeartClick(answerId) so the parent
receives the answerId; also update any callers/consumers of MyAnswerItem to
accept the new (Long) parameter.
In `@app/src/main/java/com/byeboo/app/presentation/quest/QuestViewModel.kt`:
- Around line 450-452: The onHeartClicked() handler in QuestViewModel is a
no-op; implement it to at minimum toggle the local heart state and emit a UI
event so the view shows immediate feedback: update the backing state flow/live
data (e.g., _isHearted / isHearted) to the opposite value, optimistically call
the repository method (e.g., questRepository.toggleHeart / likeQuest) in a
coroutine and handle success/error, and emit a one-off UI event (e.g.,
_uiEvent.postValue(SnackBarMessage(...)) or send via _eventFlow) on error to
revert state and inform the user; ensure you reference onHeartClicked, the
backing state property (isHearted/_isHearted) and your UI event channel
(uiEvent/_uiEvent/_eventFlow) when making the changes.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerViewModel.kt`:
- Around line 94-105: Update onHeartClicked to adjust heartCount in the same
_uiState.update where isLiked is toggled: when retrieving state.answer (in
onHeartClicked), compute newIsLiked = !answer.isLiked and newHeartCount = if
(newIsLiked) answer.heartCount + 1 else max(0, answer.heartCount - 1), then
return state.copy(answer = answer.copy(isLiked = newIsLiked, heartCount =
newHeartCount)); keep the null-safe return@update state behavior and do the
update atomically inside _uiState.update.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.kt`:
- Around line 88-90: The update that maps detail.answer into the UI model only
sets writtenAt and content, leaving reaction fields stale; update the mapping in
MyDetailAnswerViewModel where the answer is constructed (the block that
currently sets writtenAt = mapper.formatDetailDate(detail.answer.writtenAt),
content = detail.answer.content) to also copy isLiked, heartCount and
commentCount from detail.answer (e.g., isLiked = detail.answer.isLiked,
heartCount = detail.answer.heartCount, commentCount =
detail.answer.commentCount); do the same fix in loadMyDetailAnswer() for the
answersFlow / cached update path so both live and cached models include the
reaction fields.
- Around line 151-152: The onHeartClicked handler in MyDetailAnswerViewModel is
empty; implement it to at minimum toggle the local heart state and emit UI
updates: locate the onHeartClicked function in MyDetailAnswerViewModel, flip the
existing isHearted/isLiked boolean (or add one if missing), adjust any
heartCount integer accordingly, and post the updated state through the same
observable used by the view (LiveData/StateFlow) so the UI shows immediate
feedback; additionally, if there's an event channel (e.g., SingleLiveEvent or
shared Flow) for side-effects like network requests, send a follow-up event
(e.g., HeartToggled) so the caller can perform the server update asynchronously.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/MyAnswerViewModel.kt`:
- Around line 39-54: MyAnswerViewModel currently commented out the collector for
questCommonRepository.answersFlow, so _uiState.answers is never updated;
re-enable or re-implement the flow collector (e.g., inside
viewModelScope.launch) to map incoming answerModels into MyAnswerModel (use
answerId, question, DateUtil.formatToDotDate(it.writtenAt), content) and set
_uiState.update { it.copy(answers = mappedList.toPersistentList()) }. Also
ensure refreshMyAnswers()/loadMyAnswers() trigger the repository to emit into
answersFlow so the UI updates when those methods return Result<Unit>.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/screen/CommonJourneyScreen.kt`:
- Around line 51-52: Change the onHeartClick parameter on CommonJourneyScreen
from a no-arg lambda to accept the answer id: update the signature onHeartClick:
(Long) -> Unit and adjust all call sites accordingly; in the card rendering
where the heart is clicked (the place that references answer), call
onHeartClick(answer.answerId) so each card passes its own answerId for API calls
and optimistic updates (see the lambda used around answer.answerId at the
current call site).
🪄 Autofix (Beta)
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
Run ID: 3f072d3b-9c93-4ba6-a2d2-653916ebcd80
📒 Files selected for processing (24)
app/src/main/java/com/byeboo/app/data/dto/response/quest/CommonQuestResponseDto.ktapp/src/main/java/com/byeboo/app/data/mapper/quest/QuestCommonMapper.ktapp/src/main/java/com/byeboo/app/data/service/quest/QuestCommonService.ktapp/src/main/java/com/byeboo/app/domain/model/quest/CommonQuestAnswer.ktapp/src/main/java/com/byeboo/app/presentation/quest/QuestScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/QuestUiState.ktapp/src/main/java/com/byeboo/app/presentation/quest/QuestViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonWritingViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonAnswerItem.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/card/MyAnswerItem.ktapp/src/main/java/com/byeboo/app/presentation/quest/model/QuestModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/MyAnswerScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/MyAnswerViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerState.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/screen/CommonJourneyScreen.ktapp/src/main/res/drawable/ic_comment.xmlapp/src/main/res/drawable/ic_purple_heart.xmlapp/src/main/res/drawable/ic_white_heart.xmlgradle/libs.versions.toml
| onCommentClick: (Long) -> Unit = {}, | ||
| answerId: Long = 0L, | ||
| ) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
기본값(onCommentClick = {}, answerId = 0L)은 연결 누락을 숨길 수 있습니다.
이 컴포넌트는 실제 상호작용 UI라서, 기본 no-op/가짜 ID 대신 필수 파라미터로 강제하는 편이 안전합니다.
🤖 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/button/ReactionCountButton.kt`
around lines 27 - 29, The ReactionCountButton component currently hides missing
wiring by giving onCommentClick a no-op default and answerId a fake 0L default;
change the function signature for ReactionCountButton to require onCommentClick:
(Long) -> Unit and answerId: Long with no defaults (i.e., make them mandatory
parameters), then update all call sites to pass a real click handler and a valid
answerId (or propagate the required params) so missing connections are caught at
compile time rather than silently ignored.
| private fun ReactionCount( | ||
| @DrawableRes iconImg: Int, | ||
| count: Int, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| Row( | ||
| modifier = modifier, | ||
| horizontalArrangement = Arrangement.spacedBy(screenWidthDp(4.dp)), | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| ) { | ||
| Image( | ||
| imageVector = ImageVector.vectorResource(id = iconImg), | ||
| contentDescription = null, | ||
| modifier = Modifier.size(20.dp), | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.kt"
# Print file with line numbers around the reported region
if [ -f "$FILE" ]; then
echo "=== File: $FILE ==="
nl -ba "$FILE" | sed -n '1,140p'
else
echo "File not found: $FILE" >&2
exit 1
fi
# Search for semantics/contentDescription/clickable usage in this file
echo "=== Search within file ==="
rg -n "semantics|contentDescription|Role\.Button|clickable" "$FILE" || trueRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.kt"
if [ -f "$FILE" ]; then
echo "=== File: $FILE (first 200 lines) ==="
cat -n "$FILE" | sed -n '1,220p'
else
echo "File not found: $FILE" >&2
exit 1
fi
echo "=== Search within file: semantics/contentDescription/Role.Button/clickable ==="
rg -n "semantics|contentDescription|Role\.Button|Role=|role\s*=|clickable" "$FILE" || trueRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 3261
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "fun\s+Modifier\.noRippleClickable|noRippleClickable\s*\(" app/src/main/java || trueRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 3204
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app/src/main/java/com/byeboo/app/core/util/ModifierExt.kt"
if [ -f "$FILE" ]; then
echo "=== File: $FILE (first 120 lines) ==="
cat -n "$FILE" | sed -n '1,160p'
else
echo "File not found: $FILE" >&2
exit 1
fiRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1475
🌐 Web query:
Jetpack Compose Modifier.clickable semantics mergeDescendants contentDescription role Button how it reads descendant Text
💡 Result:
In Jetpack Compose, Modifier.clickable + semantics merging means accessibility services will usually “treat the subtree as one logical node” when mergeDescendants=true (directly via Modifier.semantics or implicitly via certain clickable/toggleable components). 1) How it merges descendant semantics (mergeDescendants) - When a semantics property on the parent has mergeDescendants=true, accessibility can treat the component as a single entity and merge descendant semantics properties into the parent using per-property merge policies.[1] - The Compose accessibility docs describe two trees: - a merged semantics tree (used by the testing framework by default), and - an unmerged semantics tree (keeps every node intact; accessibility services apply their own merging taking mergeDescendants into account).[2] - The docs also state that several Foundation/Material modifiers/composables have mergeDescendants enabled by default; specifically, clickable and toggleable modifiers “will automatically merge their descendants”.[2] 2) What role=Button and contentDescription do - The Role and contentDescription are semantics properties attached to the composable/modifier node. - If you use mergeDescendants, contentDescription has a defined merge behavior: it “adds all descendant ContentDescription values to a list”.[1] - Practical implication: if a parent is marked as role=Button and you don’t otherwise clear/override semantics, TalkBack may read both: - the parent’s contentDescription (if set), and - all descendants’ contentDescription values (for example, from Icon(s)).[1] - The Semantics docs also describe how content is derived for accessibility; for example, Text has a semantic property text, and Icon uses contentDescription if set by the developer.[2] 3) “How it reads descendant Text” - Text contributes semantic “text”. When the subtree is merged, the semantics properties get merged; the accessibility tree construction can therefore result in the combined label being read as part of the single merged node. - The docs explicitly note that the Button example can be interacted with by matching the text shown inside it, because descendant text is merged such that the testing framework sees the merged semantics tree.[2] 4) Default behavior when using Modifier.clickable - clickable’s API includes role: Role? as a parameter, letting you set role=Role.Button on the clickable node.[3] - Compose’s accessibility docs: clickable (and toggleable) modifiers automatically merge their descendants, so in a tree like Row(modifier=Modifier.clickable{…}) { Text(...); Text(...)} the descendant text semantics are eligible to be merged/read as part of the clickable element.[2] 5) If you want to prevent TalkBack from also reading descendant text/content descriptions - Clear and set semantics on the container to remove descendant semantics and provide a single authoritative label. - Compose provides Modifier.clearAndSetSemantics, which “clears the semantics of all descendant nodes and sets new semantics”.[4] - A StackOverflow example (using mergeDescendants=true on the parent) shows using clearAndSetSemantics to replace rather than add/merge descendant content descriptions.[5] Example pattern (replace label, don’t double-read) - Row( Modifier.semantics(mergeDescendants = true) { }.clearAndSetSemantics { contentDescription = "Open article" }) { Text("..." /* will be cleared from semantics */) } Answering your specific phrase - Modifier.clickable semantics mergeDescendants contentDescription role Button: With clickable, descendants are typically merged automatically; contentDescription merges by collecting descendant contentDescription values into a list; Text descendants provide semantic text that becomes part of the merged representation that accessibility can read as the label for the resulting role=Button node.[1][3][2] - To control exactly what is read (and avoid descendant Text being read/doubled), clear descendant semantics and set a single contentDescription on the container.[4][5]
Citations:
- 1: https://developer.android.google.cn/develop/ui/compose/accessibility/merging-clearing
- 2: https://developer.android.com/develop/ui/compose/accessibility/semantics
- 3: https://developer.android.com/reference/kotlin/androidx/compose/foundation/clickable.modifier
- 4: https://developer.android.com/reference/kotlin/androidx/compose/ui/node/SemanticsModifierNode
- 5: https://stackoverflow.com/questions/72285821/jetpack-compose-ignoring-descendants-content-description
🌐 Web query:
Modifier.clearAndSetSemantics with Modifier.clickable order semantics onClick action keeps button role
💡 Result:
In Jetpack Compose, applying Modifier.clearAndSetSemantics on a parent that also has Modifier.clickable will preserve the role you set at the clickable level, as long as you don’t clear/overwrite that role in the clearAndSetSemantics block. Key points from the official docs: 1) Modifier.clearAndSetSemantics clears semantics of the descendants and sets the semantics you provide at that node. In the merged semantics tree, it “clears the semantic information provided by the node's descendants … and sets the provided semantics” [1]. So it is specifically intended for removing/overwriting child semantics (e.g., text/icon) while keeping or setting what you want on the parent. 2) clickable can contribute a semantic role. The clickable API has a role parameter: “Role the type of user interface element. Accessibility services might use this to describe the element…” [2]. So if you set role on clickable (directly or via a toggleable), the role originates from that clickable semantics. 3) If you call clearAndSetSemantics with an empty lambda, you’re clearing descendant semantics and not setting new semantics (so you won’t replace the parent’s existing semantics such as the role already produced by clickable). The clear/overwrite guidance explains that when clearing with an empty lambda, the component’s semantics are cleared, and when overwriting content with clearAndSetSemantics{…}, “the new semantics replace all previous semantics of the element and its descendants.” [3] Therefore, the situation you described (“Modifier.clearAndSetSemantics with Modifier.clickable order semantics onClick action keeps button role”) matches the intended behavior: clearAndSetSemantics is for clearing descendant semantics, while clickable still provides the onClick action and role semantics. Practical guidance: - Apply clickable to the element that should be announced and activated. - Use Modifier.clearAndSetSemantics {} on descendant nodes (or on the parent if you only want to clear descendants) to remove text/icon announcements, but don’t overwrite the parent’s role/activation semantics unless you explicitly set them inside clearAndSetSemantics { }. - Prefer clearing descendant semantics rather than relying on mergeDescendants when you need to completely hide children semantics (docs recommend clearAndSetSemantics for “completely cleared or overwritten” semantics) [3].
Citations:
- 1: https://developer.android.com/reference/kotlin/androidx/compose/ui/semantics/clearAndSetSemantics.modifier
- 2: https://developer.android.com/reference/kotlin/androidx/compose/foundation/clickable.modifier
- 3: https://developer.android.com/develop/ui/compose/accessibility/merging-clearing
스크린리더에서 하트/댓글 버튼 구분이 안 되도록 숫자만 읽힙니다(접근성 라벨 누락).
ReactionCount는 클릭 가능(noRippleClickable=단순 clickable)하지만, Row에 semantics/contentDescription/Role.Button 라벨이 없어 아이콘(Image contentDescription=null)과 count(Text: "$count")만 노출될 가능성이 큽니다. 하트/댓글을 구분하기 어렵습니다.
수정 예시
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
`@Composable`
private fun ReactionCount(
`@DrawableRes` iconImg: Int,
+ label: String,
count: Int,
modifier: Modifier = Modifier,
) {
Row(
- modifier = modifier,
+ modifier =
+ modifier.semantics {
+ role = Role.Button
+ contentDescription = "$label $count"
+ },
horizontalArrangement = Arrangement.spacedBy(screenWidthDp(4.dp)),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = ImageVector.vectorResource(id = iconImg),
contentDescription = null,
modifier = Modifier.size(20.dp),
) ReactionCount(
iconImg = if (isLiked) R.drawable.ic_purple_heart else R.drawable.ic_white_heart,
+ label = "좋아요",
count = heartCount,
modifier =
Modifier.clickable(
onClick = onHeartClick,
),
)
ReactionCount(
iconImg = R.drawable.ic_comment,
+ label = "댓글",
count = commentCount,
modifier =
Modifier.clickable {
onCommentClick(answerId)
},
)🤖 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/button/ReactionCountButton.kt`
around lines 55 - 69, ReactionCount currently exposes only an icon (Image with
contentDescription=null) and a Text count, so screen readers read only the
number; update ReactionCount to provide an accessible label and role by adding
semantics on the clickable Row (the same composable that uses
noRippleClickable/clickable) — populate Modifier.semantics { contentDescription
= "${label} $count" ; this.role = Role.Button } where label differentiates heart
vs comment (use an input param or resource string), and keep Image
contentDescription=null to avoid duplicate announcements; ensure the semantic
contentDescription is localized (use string resources) and includes the count so
assistive tech reads e.g. "Like 3" or "Comment 2".
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/review/common/personal/detail/MyDetailAnswerViewModel.kt`:
- Around line 66-90: The success-only flow in viewModelScope.launch for
getCommonQuestAnswerDetail (the onSuccess block using detail and updating
_uiState) lacks error handling; change the coroutine flow to handle failures
(e.g., add onFailure or a try/catch around
questCommonRepository.getCommonQuestAnswerDetail(answerId)) and surface an error
to the UI by updating _uiState with an error flag/message or emitting a one-time
error event (use the same pattern used elsewhere for showing toasts/dialogs);
apply the same fix to the other identical block that calls
getCommonQuestAnswerDetail (the second viewModelScope.launch near the bottom) so
network or parsing errors do not fail silently.
- Around line 159-167: The update block in _uiState.update uses
state.answer?.copy(...) so when state.answer is null the server response is
ignored; change the assignment to build a new Answer when null (e.g., use
state.answer?.copy(...) ?: Answer(...)) so answer is always set with answerId,
displayTime (mapper.formatDetailDate(detail.answer.writtenAt)) and content from
detail.answer; locate this logic inside MyDetailAnswerViewModel's
_uiState.update and replace the state.answer?.copy(...) expression accordingly.
🪄 Autofix (Beta)
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
Run ID: 8c84deba-d45e-4381-b03b-9af6abc18809
📒 Files selected for processing (6)
app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerState.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerState.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.kt
💤 Files with no reviewable changes (1)
- app/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerState.kt
| _uiState.update { state -> | ||
| state.copy( | ||
| questQuestion = detail.question, | ||
| answer = | ||
| state.answer.copy( | ||
| state.answer?.copy( | ||
| answerId = answerId, | ||
| question = detail.question, | ||
| writtenAt = detail.writtenAt.toString(), | ||
| content = detail.content, | ||
| displayTime = mapper.formatDetailDate(detail.answer.writtenAt), | ||
| content = detail.answer.content, | ||
| ), |
There was a problem hiding this comment.
refresh 경로에서 state.answer가 null이면 상세 데이터가 반영되지 않습니다.
Line 163의 state.answer?.copy(...) 때문에 null 상태에서는 서버 응답을 받아도 answer가 계속 null로 유지됩니다.
수정 예시
.onSuccess { detail ->
+ val detailAnswer = detail.answer
_uiState.update { state ->
state.copy(
questQuestion = detail.question,
answer =
- state.answer?.copy(
- answerId = answerId,
- displayTime = mapper.formatDetailDate(detail.answer.writtenAt),
- content = detail.answer.content,
- ),
+ state.answer?.copy(
+ answerId = answerId,
+ displayTime = mapper.formatDetailDate(detailAnswer.writtenAt),
+ content = detailAnswer.content,
+ ) ?: CommonAnswerModel(
+ heartCount = detailAnswer.heartCount,
+ commentCount = detailAnswer.commentCount,
+ isLiked = detailAnswer.isLiked,
+ answerId = answerId,
+ writerId = detailAnswer.writerId,
+ writer = detailAnswer.writer,
+ profileIconRes = mapper.mapToIconRes(detailAnswer.profileIcon),
+ displayTime = mapper.formatDetailDate(detailAnswer.writtenAt),
+ content = detailAnswer.content,
+ ),
)
}
}🤖 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/personal/detail/MyDetailAnswerViewModel.kt`
around lines 159 - 167, The update block in _uiState.update uses
state.answer?.copy(...) so when state.answer is null the server response is
ignored; change the assignment to build a new Answer when null (e.g., use
state.answer?.copy(...) ?: Answer(...)) so answer is always set with answerId,
displayTime (mapper.formatDetailDate(detail.answer.writtenAt)) and content from
detail.answer; locate this logic inside MyDetailAnswerViewModel's
_uiState.update and replace the state.answer?.copy(...) expression accordingly.
znayeonzn
left a comment
There was a problem hiding this comment.
저 정말 꼼꼼히 봤는데 못 찾아냈어여,.....ㅠㅠㄹㅇCM 수고하셨습니다!!!!
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonAnswerItem.kt (1)
31-33:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift하트 콜백에
answerId가 없어 리스트 항목 식별이 불가능합니다.현재 계약(
onHeartClick: () -> Unit)으로는 상위가 “어떤 답변”의 하트 클릭인지 알 수 없습니다.onHeartClick: (Long) -> Unit로 올리고answer.answerId를 전달해 주세요.변경 예시
fun CommonAnswerItem( answer: CommonAnswerModel, - onHeartClick: () -> Unit, + onHeartClick: (Long) -> Unit, @@ ReactionCountButton( isLiked = answer.isLiked, heartCount = answer.heartCount, - onHeartClick = onHeartClick, + onHeartClick = { onHeartClick(answer.answerId) }, commentCount = answer.commentCount, onCommentClick = onCommentClick?.let { { it(answer.answerId) } }, )Also applies to: 97-101
🤖 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/CommonAnswerItem.kt` around lines 31 - 33, The heart callback lacks the answer identifier so parent cannot tell which item was clicked; update the CommonAnswerItem composable signature from onHeartClick: () -> Unit to onHeartClick: (Long) -> Unit and, wherever the heart is invoked inside CommonAnswerItem, call onHeartClick(answer.answerId). Apply the same change to the other identical occurrence around lines 97-101 (update the parameter type and pass answer.answerId when invoking) so the caller receives the answerId.
♻️ Duplicate comments (3)
app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.kt (2)
149-151:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
onHeartClicked()가 비어 있어 하트 클릭이 실제로 동작하지 않습니다.최소한 로컬 상태 토글(
isLiked,heartCount)은 즉시 반영되어야 사용자 피드백이 보장됩니다.변경 예시
+import kotlin.math.max @@ fun onHeartClicked() { - // TODO: 하트 클릭 + _uiState.update { state -> + val current = state.answer ?: return@update state + val nextLiked = !current.isLiked + val nextHeartCount = + if (nextLiked) current.heartCount + 1 + else max(0, current.heartCount - 1) + + state.copy( + answer = + current.copy( + isLiked = nextLiked, + heartCount = nextHeartCount, + ), + ) + } }🤖 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/personal/detail/MyDetailAnswerViewModel.kt` around lines 149 - 151, The onHeartClicked() in MyDetailAnswerViewModel is empty so heart taps don't update UI; implement it to immediately toggle the local state (flip isLiked and increment/decrement heartCount) and post/update the corresponding LiveData/StateFlow so the UI reflects the change optimistically, then call the repository/use-case to perform the network update (handle success silently and on failure revert the local toggle and count and surface an error if needed). Ensure you reference and update the existing properties isLiked and heartCount and call any existing method that persists the like state (or add one) from the ViewModel.
165-169:⚠️ Potential issue | 🟠 Major | ⚡ Quick winrefresh 경로에서
state.answer가 null이면 상세 응답이 유실됩니다.
state.answer?.copy(...)만 사용하면 null 상태에서 서버 성공 응답이 와도answer가 계속 null로 남습니다.변경 예시
.onSuccess { detail -> + val detailAnswer = detail.answer _uiState.update { state -> state.copy( questQuestion = detail.question, answer = state.answer?.copy( answerId = answerId, - displayTime = mapper.formatDetailDate(detail.answer.writtenAt), - content = detail.answer.content, - ), + displayTime = mapper.formatDetailDate(detailAnswer.writtenAt), + content = detailAnswer.content, + isLiked = detailAnswer.isLiked, + heartCount = detailAnswer.heartCount, + commentCount = detailAnswer.commentCount, + ) ?: CommonAnswerModel( + heartCount = detailAnswer.heartCount, + commentCount = detailAnswer.commentCount, + isLiked = detailAnswer.isLiked, + answerId = answerId, + writerId = detailAnswer.writerId, + writer = detailAnswer.writer, + profileIconRes = mapper.mapToIconRes(detailAnswer.profileIcon), + displayTime = mapper.formatDetailDate(detailAnswer.writtenAt), + content = detailAnswer.content, + ), ) } }🤖 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/personal/detail/MyDetailAnswerViewModel.kt` around lines 165 - 169, The current refresh logic uses state.answer?.copy(...), so when state.answer is null the success response won't populate answer; change the assignment to always set state.answer by using either the existing answer or constructing from detail.answer (e.g., state.copy(answer = (state.answer ?: detail.answer).copy(answerId = answerId, displayTime = mapper.formatDetailDate(detail.answer.writtenAt), content = detail.answer.content))) so a null state.answer is replaced with the new data; update the code in MyDetailAnswerViewModel where state, answer, detail.answer, mapper.formatDetailDate and answerId are referenced.app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.kt (1)
21-27: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win댓글 클릭 계약을 nullable 기본값으로 두면 연결 누락이 조용히 숨겨집니다.
ReactionCountButton가 상호작용 UI인 만큼onCommentClick을 필수 파라미터로 강제하는 편이 안전합니다. 지금 구조는 호출부 누락이 컴파일 타임에 잡히지 않습니다.변경 예시
fun ReactionCountButton( isLiked: Boolean, heartCount: Int, onHeartClick: () -> Unit, commentCount: Int, modifier: Modifier = Modifier, - onCommentClick: (() -> Unit)? = null, + onCommentClick: () -> Unit, ) { @@ ReactionCount( iconImg = R.drawable.ic_comment, count = commentCount, - modifier = - if (onCommentClick != null) { - Modifier.noRippleClickable { onCommentClick() } - } else { - Modifier - }, + modifier = Modifier.noRippleClickable { onCommentClick() }, )Also applies to: 46-50
🤖 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/button/ReactionCountButton.kt` around lines 21 - 27, The onCommentClick parameter in the ReactionCountButton composable is currently nullable with a default, which hides missing click handlers; change the signature of ReactionCountButton to make onCommentClick a required non-nullable (() -> Unit) parameter (remove the default and nullable type) and update all call sites that relied on the default to pass an explicit click lambda (or provide a wrapper overload if necessary); also apply the same change to the second overload mentioned (lines 46-50) so callers must explicitly supply onCommentClick and the compiler will catch missing handlers.
🤖 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.
Outside diff comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonAnswerItem.kt`:
- Around line 31-33: The heart callback lacks the answer identifier so parent
cannot tell which item was clicked; update the CommonAnswerItem composable
signature from onHeartClick: () -> Unit to onHeartClick: (Long) -> Unit and,
wherever the heart is invoked inside CommonAnswerItem, call
onHeartClick(answer.answerId). Apply the same change to the other identical
occurrence around lines 97-101 (update the parameter type and pass
answer.answerId when invoking) so the caller receives the answerId.
---
Duplicate comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.kt`:
- Around line 21-27: The onCommentClick parameter in the ReactionCountButton
composable is currently nullable with a default, which hides missing click
handlers; change the signature of ReactionCountButton to make onCommentClick a
required non-nullable (() -> Unit) parameter (remove the default and nullable
type) and update all call sites that relied on the default to pass an explicit
click lambda (or provide a wrapper overload if necessary); also apply the same
change to the second overload mentioned (lines 46-50) so callers must explicitly
supply onCommentClick and the compiler will catch missing handlers.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.kt`:
- Around line 149-151: The onHeartClicked() in MyDetailAnswerViewModel is empty
so heart taps don't update UI; implement it to immediately toggle the local
state (flip isLiked and increment/decrement heartCount) and post/update the
corresponding LiveData/StateFlow so the UI reflects the change optimistically,
then call the repository/use-case to perform the network update (handle success
silently and on failure revert the local toggle and count and surface an error
if needed). Ensure you reference and update the existing properties isLiked and
heartCount and call any existing method that persists the like state (or add
one) from the ViewModel.
- Around line 165-169: The current refresh logic uses state.answer?.copy(...),
so when state.answer is null the success response won't populate answer; change
the assignment to always set state.answer by using either the existing answer or
constructing from detail.answer (e.g., state.copy(answer = (state.answer ?:
detail.answer).copy(answerId = answerId, displayTime =
mapper.formatDetailDate(detail.answer.writtenAt), content =
detail.answer.content))) so a null state.answer is replaced with the new data;
update the code in MyDetailAnswerViewModel where state, answer, detail.answer,
mapper.formatDetailDate and answerId are referenced.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 82ce87d3-37d3-4303-a915-9487a67e35b4
📒 Files selected for processing (4)
app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonAnswerItem.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/card/MyAnswerItem.ktapp/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.kt
|
안녕하세여 소희님. 잘봣습니당. 피알제목에 슬래시가 빠진거가튼데.고쳐주세염. |
ㅋㅋㅋㅋㅋ했습니다~~ |
Related issue 🛠
Work Description 📝
Screenshot 📸
Screen_recording_20260525_011743.mp4
-> 곧 첨부하겠습니다~
Uncompleted Tasks 😅
PR Point 📌
컴포넌트 디자인 수정 작업만 하려고 했었는데요.
이미 화면에서 사용하던 것들이여서 디자인 수정을 하니까 관련해서 모델, uiState 다 수정해줘야 되가지고 일단 일차적으로 다 수정해놨습니다!
하트수, 댓글 수는 일단 고정값으로 해놨습니다.
추후 서버 연결할 때 작업할 예정이고, 하트 클릭 아이콘 변경도 그때 같이 반영하도록 할게요!
트러블 슈팅 💥
Summary by CodeRabbit
새로운 기능
개선사항
기타