Skip to content

[chore/#315] 공통 답장 컴포넌트 변경#321

Open
sohee6989 wants to merge 17 commits into
developfrom
chore#315-change-common-answer-component
Open

[chore/#315] 공통 답장 컴포넌트 변경#321
sohee6989 wants to merge 17 commits into
developfrom
chore#315-change-common-answer-component

Conversation

@sohee6989
Copy link
Copy Markdown
Collaborator

@sohee6989 sohee6989 commented May 23, 2026

Related issue 🛠

Work Description 📝

  • 공통 답장 컴포넌트 변경

Screenshot 📸

Screen_recording_20260525_011743.mp4

-> 곧 첨부하겠습니다~

Uncompleted Tasks 😅

  • 내 답변 모아보기
  • 하트 클릭

PR Point 📌

컴포넌트 디자인 수정 작업만 하려고 했었는데요.
이미 화면에서 사용하던 것들이여서 디자인 수정을 하니까 관련해서 모델, uiState 다 수정해줘야 되가지고 일단 일차적으로 다 수정해놨습니다!

하트수, 댓글 수는 일단 고정값으로 해놨습니다.
추후 서버 연결할 때 작업할 예정이고, 하트 클릭 아이콘 변경도 그때 같이 반영하도록 할게요!

트러블 슈팅 💥

Summary by CodeRabbit

  • 새로운 기능

    • 답변에 좋아요(하트) 상태, 좋아요/댓글 수 표시 및 하트 클릭 상호작용(즉시 토글) 추가
    • 하트·댓글 카운트 UI 컴포넌트 및 하트/댓글 아이콘 리소스 추가
  • 개선사항

    • 답변 상세 구조(답변/댓글) 개선으로 상세 정보 및 시간 표시 일관화
    • 여러 화면(목록/상세/내 답변)에 하트·댓글 핸들러 연결
    • 퀘스트 API 경로 v1 → v2, 앱 버전 코드 10 → 12 적용
  • 기타

    • 일부 상태 필드 및 모델이 갱신되어 UI 바인딩 정리 (기존 식별자 위치 조정, TODO 핸들러 포함)

Review Change Stack

@sohee6989 sohee6989 linked an issue May 23, 2026 that may be closed by this pull request
1 task
@auto-assign auto-assign Bot requested review from fredleeJH and znayeonzn May 23, 2026 16:25
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Walkthrough

공통 퀘스트 답변에 하트·댓글 집계와 좋아요 상태를 추가했습니다. API DTO·매퍼·도메인·프레젠테이션 모델, Compose UI(ReactionCountButton), 화면과 뷰모델 연결 및 Retrofit 경로를 v2로 갱신했습니다.

Changes

하트/댓글 반응 기능

Layer / File(s) Summary
응답 DTO 확장 및 상세 DTO 도입
app/src/main/java/com/byeboo/app/data/dto/response/quest/CommonQuestResponseDto.kt
QuestAnswerDto와 상세 응답 DTO에 likeCount, commentCount, isLiked 필드 추가, 상세 answer/comments 타입 도입.
매퍼 로직 및 writtenAt 파싱 변경
app/src/main/java/com/byeboo/app/data/mapper/quest/QuestCommonMapper.kt
DTO→도메인 매핑에 answer/comments 변환 함수 추가, writtenAt 파싱 헬퍼(ISO_LOCAL_DATE_TIME/ISO_LOCAL_DATE 분기) 도입.
Retrofit 엔드포인트 버전 갱신
app/src/main/java/com/byeboo/app/data/service/quest/QuestCommonService.kt
공통 퀘스트 조회/상세 @GET 경로를 /api/v1/...에서 /api/v2/...로 변경.
도메인 모델 확장
app/src/main/java/com/byeboo/app/domain/model/quest/CommonQuestAnswer.kt
CommonQuestAnswer 및 상세 모델에 heartCount, commentCount, isLiked, 댓글의 commentId 등 필드 추가.
프레젠테이션 모델·상태 업데이트
app/src/main/java/com/byeboo/app/presentation/quest/model/QuestModel.kt, app/src/main/java/com/byeboo/app/presentation/quest/QuestUiState.kt, app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerState.kt
CommonAnswerModel/MyAnswerModel, CommonJourneyState, MyDetailAnswerState 등에 반응 관련 프로퍼티 추가 및 일부 상태 타입 변경.
ReactionCountButton 및 리소스 추가
app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.kt, app/src/main/res/drawable/ic_purple_heart.xml, app/src/main/res/drawable/ic_white_heart.xml, app/src/main/res/drawable/ic_comment.xml
하트/댓글 카운트를 표시하는 Composable 추가. isLiked에 따라 아이콘(보라/흰색 하트) 분기, 클릭 핸들러 바인딩. 신규 벡터 리소스 추가.
답변 카드 컴포넌트 통합
app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonAnswerItem.kt, app/src/main/java/com/byeboo/app/presentation/quest/component/card/MyAnswerItem.kt
CommonAnswerItem/MyAnswerItem 시그니처에 onHeartClick/onCommentClick 추가, 하단에 ReactionCountButton 렌더링 및 댓글 클릭에 answerId 바인딩.
스크린·뷰모델 라우팅 및 핸들러 연결
app/src/main/java/com/byeboo/app/presentation/quest/QuestScreen.kt, app/src/main/java/com/byeboo/app/presentation/quest/screen/CommonJourneyScreen.kt, app/src/main/java/com/byeboo/app/presentation/quest/QuestViewModel.kt
스크린과 Route에서 onHeartClick을 viewModel::onHeartClicked로 전달. ViewModel에서 CommonAnswerModel 매핑에 반응 필드 포함, onHeartClicked() 추가(현재 스텁).
타인 답변 상세 흐름
app/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerScreen.kt, app/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerViewModel.kt
상세 매핑을 domainModel.answer 기반으로 정리하고 onHeartClicked로 isLiked 토글 로직 적용(로컬 상태 토글).
내 답변 목록 및 상세 흐름
app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/MyAnswerScreen.kt, app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/MyAnswerViewModel.kt, app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerScreen.kt, app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.kt
MyAnswerScreen/MyDetailAnswerScreen에 onHeartClick 전달 및 CommonAnswerItem 사용으로 상세 렌더링 통일. ViewModel들에 onHeartClicked() 추가(일부는 TODO) 및 로딩/리프레시 매핑 조정.
편집 로직 조정 및 버전 업데이트
app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonWritingViewModel.kt, gradle/libs.versions.toml
QuestCommonWritingViewModel에서 detail.answer.content 사용으로 조정. versionCode 10→12 업데이트.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

🍥 [UI], 🌎 서버 통신

Suggested reviewers

  • fredleeJH
  • znayeonzn

Poem

"나는 토끼, 코드 숲을 폴짝폴짝,
하트가 더해져 답변이 반짝,
카운트가 춤추고 클릭은 울려,
뷰모델로 건너가 서버에 닿길,
작은 당근 대신 리뷰 한 번 부탁해요! 🐰💜"

🚥 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
Description check ✅ Passed PR 설명이 저장소의 필수 템플릿 섹션(Related issue, Work Description, Screenshot, Uncompleted Tasks, PR Point, 트러블 슈팅)을 모두 포함하고 있으며, 작업 내용과 향후 계획을 명확히 기술하고 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 연결된 issue #315의 요구사항인 '공통여정-답변 컴포넌트 UI 변경'을 충족하고 있으며, 관련 모델, UI 상태, 컴포넌트가 모두 일관되게 업데이트되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 공통 답변 컴포넌트 UI 변경 및 관련 데이터 모델 업데이트라는 범위 내에 있으며, 불필요한 범위 외 변경사항은 없습니다.
Title check ✅ Passed PR 제목은 공통 답장 컴포넌트 변경이라는 주요 작업을 명확하게 요약하고 있으며, 제공된 변경 사항들(UI 컴포넌트 구조 변경, 관련 모델 및 상태 업데이트 등)과 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore#315-change-common-answer-component

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 and usage tips.

@sohee6989 sohee6989 added 🐶 소희 소희 🛠️ [CHORE] 별거 아닌 수정 labels May 23, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 071668c and df8088b.

📒 Files selected for processing (24)
  • app/src/main/java/com/byeboo/app/data/dto/response/quest/CommonQuestResponseDto.kt
  • app/src/main/java/com/byeboo/app/data/mapper/quest/QuestCommonMapper.kt
  • app/src/main/java/com/byeboo/app/data/service/quest/QuestCommonService.kt
  • app/src/main/java/com/byeboo/app/domain/model/quest/CommonQuestAnswer.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/QuestScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/QuestUiState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/QuestViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonWritingViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonAnswerItem.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/card/MyAnswerItem.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/model/QuestModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/MyAnswerScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/MyAnswerViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/screen/CommonJourneyScreen.kt
  • app/src/main/res/drawable/ic_comment.xml
  • app/src/main/res/drawable/ic_purple_heart.xml
  • app/src/main/res/drawable/ic_white_heart.xml
  • gradle/libs.versions.toml

Comment on lines +27 to +29
onCommentClick: (Long) -> Unit = {},
answerId: Long = 0L,
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +55 to +69
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),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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" || true

Repository: 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" || true

Repository: 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 || true

Repository: 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
fi

Repository: 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:


🌐 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:


스크린리더에서 하트/댓글 버튼 구분이 안 되도록 숫자만 읽힙니다(접근성 라벨 누락).

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".

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between df8088b and 31f219b.

📒 Files selected for processing (6)
  • app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/other/CommonOtherAnswerViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerState.kt
  • app/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

Comment on lines 159 to 167
_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,
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Copy link
Copy Markdown
Collaborator

@znayeonzn znayeonzn left a comment

Choose a reason for hiding this comment

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

저 정말 꼼꼼히 봤는데 못 찾아냈어여,.....ㅠㅠㄹㅇCM 수고하셨습니다!!!!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 win

refresh 경로에서 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

📥 Commits

Reviewing files that changed from the base of the PR and between 31f219b and c72b2c8.

📒 Files selected for processing (4)
  • app/src/main/java/com/byeboo/app/presentation/quest/component/button/ReactionCountButton.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/card/CommonAnswerItem.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/card/MyAnswerItem.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/review/common/personal/detail/MyDetailAnswerViewModel.kt

@y-eonee
Copy link
Copy Markdown
Member

y-eonee commented May 26, 2026

안녕하세여 소희님. 잘봣습니당. 피알제목에 슬래시가 빠진거가튼데.고쳐주세염.

@sohee6989 sohee6989 changed the title [chore#315] 공통 답장 컴포넌트 변경 [chore/#315] 공통 답장 컴포넌트 변경 May 27, 2026
@sohee6989
Copy link
Copy Markdown
Collaborator Author

안녕하세여 소희님. 잘봣습니당. 피알제목에 슬래시가 빠진거가튼데.고쳐주세염.

ㅋㅋㅋㅋㅋ했습니다~~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐶 소희 소희 🛠️ [CHORE] 별거 아닌 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[chore] 공통여정-답변 컴포넌트 변경

3 participants