Conversation
Walkthrough퀘스트 작성 UI를 세분화된 패키지 구조로 재구조화하고, 기록형·행동형·공통 퀘스트별 전용 작성 화면을 도입했습니다. 공유 UI 컴포넌트(TopBar, Footer, Title)를 추가하고, QuestTextField를 단순화하며, 새로운 QuestCommonRoute/ViewModel을 통해 공통 퀘스트 흐름을 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant UI as 행동형<br/>작성화면
participant VM as QuestBehavior<br/>ViewModel
participant Repo as 저장소
participant Server as 서버
User->>UI: 텍스트/이미지 입력
UI->>VM: updateContent() / updateSelectedImage()
VM->>VM: 상태 업데이트
User->>UI: 완료 버튼 클릭
UI->>VM: onCompleteClicked()
alt 편집 모드
VM->>VM: onSaveEditClicked()
VM->>Repo: 이미지 업로드 또는 변경 없이 저장
else 신규 작성
VM->>VM: openBottomSheet()
User->>UI: 감정 선택
UI->>VM: updateSelectedEmotion()
end
VM->>Repo: onSaveClicked() - 콘텐츠 저장
Repo->>Server: 업로드 요청
Server-->>Repo: 성공
Repo-->>VM: 완료
VM->>VM: showCompleteModal = true
UI->>User: 완료 모달 표시
Note over VM: 2초 타임아웃
VM->>VM: onCompleteModalTimeout()
VM->>UI: NavigateToQuestBehaviorComplete
sequenceDiagram
participant User as 사용자
participant QuestScreen as 퀘스트<br/>메인 화면
participant NavHost as 네비게이션<br/>호스트
participant CommonScreen as 공통 퀘스트<br/>작성 화면
participant CommonVM as QuestCommon<br/>ViewModel
User->>QuestScreen: 공통 퀘스트 클릭
QuestScreen->>NavHost: navigateToQuestCommon(questId)
NavHost->>CommonScreen: 라우트 전환
CommonScreen->>CommonVM: collectAsStateWithLifecycle()
User->>CommonScreen: 텍스트 입력
CommonScreen->>CommonVM: updateContent(isFocused, text)
CommonVM->>CommonVM: QuestContentLengthValidator 검증
CommonVM->>CommonVM: uiState 업데이트
CommonScreen->>CommonScreen: isCompleteButtonEnabled 계산
User->>CommonScreen: 완료 클릭
CommonScreen->>CommonVM: onCompleteClicked()
CommonVM->>NavHost: navigateToQuestCommonComplete()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 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 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: 7
🧹 Nitpick comments (7)
app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingViewModel.kt (1)
33-35: Repository 의존성들이private이 아닌val로 선언되어 외부에 노출됩니다.
questDetailRecordingRepository,questRecordingRepository,questRecordedDetailRepository가val로 선언되어 ViewModel 외부에서 직접 접근 가능합니다.mixpanelUtil처럼private val로 변경하여 캡슐화를 유지하는 것이 좋습니다.♻️ 수정 제안
`@Inject` constructor( - val questDetailRecordingRepository: QuestDetailRecordingRepository, - val questRecordingRepository: QuestRecordingRepository, - val questRecordedDetailRepository: QuestRecordedDetailRepository, + private val questDetailRecordingRepository: QuestDetailRecordingRepository, + private val questRecordingRepository: QuestRecordingRepository, + private val questRecordedDetailRepository: QuestRecordedDetailRepository, savedStateHandle: SavedStateHandle, private val mixpanelUtil: MixpanelUtil,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingViewModel.kt` around lines 33 - 35, The three repository constructor properties questDetailRecordingRepository, questRecordingRepository, and questRecordedDetailRepository are declared as public val and are exposed outside the ViewModel; change them to private val (matching mixpanelUtil) in the QuestRecordingViewModel primary constructor to encapsulate dependencies, leaving all internal usages unchanged and updating any external accessors/calls to use exposed methods instead of direct property access.app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt (1)
65-88:collectLatest대신collect사용을 고려해 주세요.Side effect 처리에
collectLatest를 사용하면, 이전 side effect 처리 중 새 이벤트가 발생할 경우 이전 처리가 취소됩니다. 네비게이션 이벤트와 스낵바 이벤트가 거의 동시에 발생할 가능성은 낮지만, 모든 side effect가 누락 없이 처리되어야 한다면collect가 더 안전합니다.♻️ 수정 제안
LaunchedEffect(Unit) { - viewModel.sideEffect.collectLatest { effect -> + viewModel.sideEffect.collect { effect -> when (effect) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt` around lines 65 - 88, Replace the use of collectLatest with collect when collecting viewModel.sideEffect inside the LaunchedEffect block so side effects aren't cancelled mid-processing; specifically update the collector call in the LaunchedEffect that currently calls viewModel.sideEffect.collectLatest { ... } to use collect { ... } so all QuestRecordingSideEffect cases (e.g., NavigateToQuest, NavigateToQuestTip, NavigateToQuestRecordingComplete, NavigateToQuestReview, NavigateUp, ShowSnackBar) are delivered and handled reliably by the existing navigateToQuest(), navigateToQuestTip(...), navigateToQuestRecordingComplete(...), navigateToQuestReview(...), navigateUp(), and showSnackBar(...) functions.app/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteViewModel.kt (1)
37-38:uiState프로퍼티의asStateFlow()호출 방식이 다른 ViewModel과 불일치합니다.
get() = _uiState.asStateFlow()는 접근할 때마다 새로운 래퍼를 생성합니다.QuestRecordingViewModel(line 54)에서는val uiState: StateFlow<...> = _uiState.asStateFlow()로 한 번만 생성하는 패턴을 사용하고 있으므로, 일관성을 위해 동일한 패턴으로 맞추는 것을 권장합니다.♻️ 수정 제안
- val uiState: StateFlow<QuestRecordingCompleteState> - get() = _uiState.asStateFlow() + val uiState: StateFlow<QuestRecordingCompleteState> = _uiState.asStateFlow()Based on learnings: ByeBoo 프로젝트에서는 ViewModel의 StateFlow 노출 시 일관성을 위해 asStateFlow()를 사용하여 명시적으로 불변성을 보장하는 패턴을 선호한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteViewModel.kt` around lines 37 - 38, The uiState property currently exposes _uiState with a getter that calls _uiState.asStateFlow() on every access, creating a new wrapper each time; change the declaration in QuestRecordingCompleteViewModel to initialize uiState once by assigning val uiState: StateFlow<QuestRecordingCompleteState> = _uiState.asStateFlow() (matching the pattern used in QuestRecordingViewModel) so the immutable StateFlow wrapper is created only once and consistency is maintained.app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt (2)
55-63:buildAnnotatedString불필요 — 단순 문자열 템플릿으로 대체 가능스타일 스팬(
withStyle,addStyle등)이 없으므로buildAnnotatedString을 사용할 이유가 없습니다.♻️ 제안 수정
- Text( - text = - buildAnnotatedString { - append(text = currentCharCount.toString()) - append(text = "/") - append(text = maxCharCount.toString()) - }, + Text( + text = "$currentCharCount/$maxCharCount", style = ByeBooTheme.typography.body6, color = ByeBooTheme.colors.gray400, textAlign = TextAlign.End, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt` around lines 55 - 63, The Text composable uses buildAnnotatedString unnecessarily to render a simple "current/max" value; replace the buildAnnotatedString block in QuestWritingFooter.kt with a plain string template (e.g., "${currentCharCount}/${maxCharCount}") and remove any now-unused import related to buildAnnotatedString; locate the Text(...) call that references currentCharCount and maxCharCount and update its text parameter accordingly.
31-35: horizontal padding이24.dp하드코딩 —screenWidthDp와 불일치같은 컴포넌트 안에서
Spacer의 너비는screenWidthDp(3.dp)로 비율 스케일링을 적용하면서, Row의 horizontal padding만 고정 값인24.dp를 사용해 반응형 레이아웃 일관성이 깨집니다.♻️ 제안 수정
- .padding(horizontal = 24.dp), + .padding(horizontal = screenWidthDp(24.dp)),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt` around lines 31 - 35, Row’s horizontal padding is hardcoded to 24.dp while the same component uses scaled spacing via screenWidthDp(3.dp) for the Spacer, breaking responsive consistency; update the Row modifier (.padding(horizontal = 24.dp)) to use the same scaling helper (e.g., .padding(horizontal = screenWidthDp(3.dp)) or the appropriate screenWidthDp(<dp>) value matching design) so the Row padding and the Spacer use the same screenWidthDp-based scaling (refer to QuestWritingFooter.kt Row modifier, the screenWidthDp(...) helper, and the Spacer call).app/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.kt (1)
25-26: Row에fillMaxWidth()미적용 — 의도 불명확
Spacer(Modifier.weight(1f))가 올바르게 동작하려면 Row가 부모의 bounded max width를 사용해야 합니다. 현재는 부모 컨텍스트에 암묵적으로 의존하고 있으므로fillMaxWidth()를 명시하는 것이 안전합니다.♻️ 제안 수정
Row( - modifier = Modifier.padding(horizontal = screenWidthDp(22.dp)), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = screenWidthDp(22.dp)), verticalAlignment = Alignment.CenterVertically, ) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.kt` around lines 25 - 26, Row의 Modifier가 부모의 가로 제약을 보장하지 않아 Spacer(Modifier.weight(1f))가 의도대로 동작하지 않을 수 있습니다; QuestWritingTopbar 컴포저블에서 Row의 Modifier 체인(현재 Modifier.padding(...))에 fillMaxWidth()를 추가하여 Row가 부모의 최대 너비를 사용하도록 명시적으로 설정하세요.app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt (1)
33-33:lastLineBottom상태가 계산되지만 어디서도 사용되지 않음 — 불필요한 코드
onTextLayout에서 매 레이아웃마다 값을 갱신하지만,lastLineBottom을 읽는 곳이 없어 불필요한 상태 오버헤드가 발생합니다.♻️ 제안 수정
- val lastLineBottom = remember { mutableStateOf(0) } LaunchedEffect(value) { scrollState.animateScrollTo(scrollState.maxValue) } BasicTextField( ... - onTextLayout = { layoutResult -> - lastLineBottom.value = - layoutResult.getLineBottom(layoutResult.lineCount - 1).toInt() - }, )Also applies to: 70-73
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt` at line 33, The mutable state lastLineBottom in the QuestTextField composable is computed in onTextLayout but never read, so remove the unnecessary state and its updates: delete the declaration val lastLineBottom = remember { mutableStateOf(0) } and remove any assignments to lastLineBottom.value inside onTextLayout (lines around the onTextLayout lambda). If you still need the computed bottom for debugging or local use, replace updates with a local (non-remember) variable inside onTextLayout; otherwise simply remove the onTextLayout code that only exists to set lastLineBottom.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/QuestBehaviorWritingScreen.kt`:
- Around line 167-168: The created rememberScrollState() stored in variable
scrollState (used in QuestTextField) isn't connected to the parent LazyColumn so
animateScrollTo on it does nothing; replace this approach by using the
LazyColumn's LazyListState and call LazyListState.animateScrollToItem(...) to
scroll the list to the item index that contains the QuestTextField (or remove
the unused rememberScrollState and rely on the existing BringIntoViewRequester
mechanism in QuestTextField); update usages where
scrollState.animateScrollTo(...) is called to instead obtain the item index
(e.g., via key or currentVisibleItemIndex) and call
lazyListState.animateScrollToItem(index).
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.kt`:
- Around line 40-47: The Text "완료" currently uses Modifier.clickable(onClick =
onCompleteClick) but ignores isEnabled so onCompleteClick is invoked even when
disabled; update the Modifier.clickable call in QuestWritingTopbar (the Text
composable) to pass enabled = isEnabled (e.g., Modifier.clickable(enabled =
isEnabled, onClick = onCompleteClick)) so clicks are disabled when isEnabled is
false and the UI behavior matches the color state.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingTitle.kt`:
- Around line 54-59: The MiddleTag invocation passes text = "작성 TIP" but
MiddleTagType.QUEST_TIP is defined with dynamicContent = false so the text
parameter is ignored; either remove the unused text argument from the MiddleTag
call (in QuestWritingTitle.kt) or change the MiddleTagType.QUEST_TIP definition
to dynamicContent = true if you intend to use the provided text; update the call
site (MiddleTag(... middleTagType = MiddleTagType.QUEST_TIP, modifier =
Modifier.clickable(onClick = onTipClick)) ) or modify the MiddleTagType
enum/definition accordingly so the provided text is actually used.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt`:
- Around line 60-69: The decorationBox currently calls Text(placeholder) and
innerTextField() sequentially causing vertical stacking; wrap innerTextField in
a Box and overlay the placeholder conditionally so the placeholder appears on
top of the text field. Modify the decorationBox lambda in QuestTextField (use
the innerTextField parameter) to place innerTextField() inside a Box and show
Text(placeholder) inside that Box only when value.isEmpty() &&
!focusState.value, ensuring placeholder uses the same sizing/alignment as the
input.
- Line 26: QuestBehaviorWritingScreen creates and passes scrollState into
QuestTextField but never applies it to the parent so
QuestTextField.animateScrollTo has no effect; update QuestBehaviorWritingScreen
to either replace the LazyColumn with a Column and add
Modifier.verticalScroll(scrollState) or, if keeping LazyColumn, switch to a
LazyListState and adapt QuestTextField to use lazy list scrolling APIs—ensure
the symbol names referenced are QuestBehaviorWritingScreen, scrollState,
QuestTextField, verticalScroll, LazyColumn, Column, and animateScrollTo so the
parent scroll modifier matches the child scrolling calls.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteScreen.kt`:
- Around line 144-153: The Spacer with Modifier.weight(1f) inside the LazyColumn
item won't push ByeBooButton to the bottom because LazyColumn's item Column is
unbounded; move the ByeBooButton out of the LazyColumn (e.g., wrap the
LazyColumn and button in a parent Column, give the LazyColumn
Modifier.weight(1f) so it takes remaining space and places ByeBooButton at
bottom) or replace the LazyColumn with a regular Column + verticalScroll and
keep Spacer(Modifier.weight(1f)) to push the button down; also replace the empty
onClick = {} on ByeBooButton with the appropriate click handler (or a TODO
callback) so the "보리에게 답장 받기" action is implemented.
- Around line 113-156: The UI uses a LazyColumn with a single item which is
unnecessary; in QuestRecordingCompleteScreen replace the LazyColumn block with a
top-level Column that contains a scrollable content area (use
Modifier.weight(1f) on the scrollable container combined with
Modifier.verticalScroll(rememberScrollState())) holding QuestTitle, ContentText,
QuestEmotionDescriptionContent and spacing, and place ByeBooButton as a sibling
below that scrollable area so the button stays anchored at the bottom; remove
the inner Spacer(weight(1f)) and adjust modifiers so weight is applied to the
scrollable container (not to children) to fix layout/scroll behavior.
---
Nitpick comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.kt`:
- Around line 25-26: Row의 Modifier가 부모의 가로 제약을 보장하지 않아
Spacer(Modifier.weight(1f))가 의도대로 동작하지 않을 수 있습니다; QuestWritingTopbar 컴포저블에서 Row의
Modifier 체인(현재 Modifier.padding(...))에 fillMaxWidth()를 추가하여 Row가 부모의 최대 너비를
사용하도록 명시적으로 설정하세요.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt`:
- Around line 55-63: The Text composable uses buildAnnotatedString unnecessarily
to render a simple "current/max" value; replace the buildAnnotatedString block
in QuestWritingFooter.kt with a plain string template (e.g.,
"${currentCharCount}/${maxCharCount}") and remove any now-unused import related
to buildAnnotatedString; locate the Text(...) call that references
currentCharCount and maxCharCount and update its text parameter accordingly.
- Around line 31-35: Row’s horizontal padding is hardcoded to 24.dp while the
same component uses scaled spacing via screenWidthDp(3.dp) for the Spacer,
breaking responsive consistency; update the Row modifier (.padding(horizontal =
24.dp)) to use the same scaling helper (e.g., .padding(horizontal =
screenWidthDp(3.dp)) or the appropriate screenWidthDp(<dp>) value matching
design) so the Row padding and the Spacer use the same screenWidthDp-based
scaling (refer to QuestWritingFooter.kt Row modifier, the screenWidthDp(...)
helper, and the Spacer call).
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt`:
- Line 33: The mutable state lastLineBottom in the QuestTextField composable is
computed in onTextLayout but never read, so remove the unnecessary state and its
updates: delete the declaration val lastLineBottom = remember {
mutableStateOf(0) } and remove any assignments to lastLineBottom.value inside
onTextLayout (lines around the onTextLayout lambda). If you still need the
computed bottom for debugging or local use, replace updates with a local
(non-remember) variable inside onTextLayout; otherwise simply remove the
onTextLayout code that only exists to set lastLineBottom.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteViewModel.kt`:
- Around line 37-38: The uiState property currently exposes _uiState with a
getter that calls _uiState.asStateFlow() on every access, creating a new wrapper
each time; change the declaration in QuestRecordingCompleteViewModel to
initialize uiState once by assigning val uiState:
StateFlow<QuestRecordingCompleteState> = _uiState.asStateFlow() (matching the
pattern used in QuestRecordingViewModel) so the immutable StateFlow wrapper is
created only once and consistency is maintained.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt`:
- Around line 65-88: Replace the use of collectLatest with collect when
collecting viewModel.sideEffect inside the LaunchedEffect block so side effects
aren't cancelled mid-processing; specifically update the collector call in the
LaunchedEffect that currently calls viewModel.sideEffect.collectLatest { ... }
to use collect { ... } so all QuestRecordingSideEffect cases (e.g.,
NavigateToQuest, NavigateToQuestTip, NavigateToQuestRecordingComplete,
NavigateToQuestReview, NavigateUp, ShowSnackBar) are delivered and handled
reliably by the existing navigateToQuest(), navigateToQuestTip(...),
navigateToQuestRecordingComplete(...), navigateToQuestReview(...), navigateUp(),
and showSnackBar(...) functions.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingViewModel.kt`:
- Around line 33-35: The three repository constructor properties
questDetailRecordingRepository, questRecordingRepository, and
questRecordedDetailRepository are declared as public val and are exposed outside
the ViewModel; change them to private val (matching mixpanelUtil) in the
QuestRecordingViewModel primary constructor to encapsulate dependencies, leaving
all internal usages unchanged and updating any external accessors/calls to use
exposed methods instead of direct property access.
| val scrollState = rememberScrollState() | ||
|
|
There was a problem hiding this comment.
scrollState가 LazyColumn과 연결되지 않아 "타이핑 시 자동 스크롤" 기능이 동작하지 않음
Line 167에서 생성한 rememberScrollState()는 QuestTextField에 전달되지만, 부모 스크롤 컨테이너인 LazyColumn은 자체 LazyListState를 사용하므로 이 scrollState를 사용하지 않습니다. 결과적으로 QuestTextField 내부의 LaunchedEffect에서 scrollState.animateScrollTo(scrollState.maxValue)를 호출해도 아무런 화면 스크롤이 발생하지 않습니다.
LazyColumn의 아이템 스크롤을 원한다면LazyListState.animateScrollToItem()을 사용해야 합니다.- 텍스트 입력 필드가 키보드에 가려지지 않도록 하는 것이 목적이라면 이미 Lines 154–165의
BringIntoViewRequester메커니즘이 그 역할을 합니다.
#!/bin/bash
# QuestRecordingScreen (writing)에서 scrollState가 verticalScroll과 연결되어 있는지 확인
fd -e kt "QuestRecordingScreen" --exec grep -n "verticalScroll\|rememberScrollState\|QuestTextField" {}Also applies to: 348-348
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/QuestBehaviorWritingScreen.kt`
around lines 167 - 168, The created rememberScrollState() stored in variable
scrollState (used in QuestTextField) isn't connected to the parent LazyColumn so
animateScrollTo on it does nothing; replace this approach by using the
LazyColumn's LazyListState and call LazyListState.animateScrollToItem(...) to
scroll the list to the item index that contains the QuestTextField (or remove
the unused rememberScrollState and rely on the existing BringIntoViewRequester
mechanism in QuestTextField); update usages where
scrollState.animateScrollTo(...) is called to instead obtain the item index
(e.g., via key or currentVisibleItemIndex) and call
lazyListState.animateScrollToItem(index).
app/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.kt
Show resolved
Hide resolved
app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingTitle.kt
Outdated
Show resolved
Hide resolved
| questWritingState: QuestWritingState, | ||
| value: String, | ||
| onValueChange: (String) -> Unit, | ||
| scrollState: ScrollState, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find QuestTextField usage and verticalScroll usage to verify the claim
rg -n "QuestTextField" --type=kt -B 3 -A 5Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 101
🏁 Script executed:
#!/bin/bash
# Find verticalScroll usage with context
rg -n "verticalScroll" --type=kt -B 5 -A 5Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 101
🏁 Script executed:
#!/bin/bash
# Also check the QuestTextField implementation itself
cat app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 2822
🏁 Script executed:
#!/bin/bash
# Find all Kotlin files
find . -name "*.kt" -type f | head -20Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1613
🏁 Script executed:
#!/bin/bash
# Search for QuestTextField usage
rg "QuestTextField" -B 3 -A 5Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 5892
🏁 Script executed:
#!/bin/bash
# Search for verticalScroll usage
rg "verticalScroll" -B 5 -A 5Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 15635
🏁 Script executed:
#!/bin/bash
# Check QuestRecordingScreen for scrollState usage
cat app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 9264
🏁 Script executed:
#!/bin/bash
# Check QuestBehaviorWritingScreen for scrollState usage
cat app/src/main/java/com/byeboo/app/presentation/quest/behavior/QuestBehaviorWritingScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 14725
QuestBehaviorWritingScreen에서 scrollState가 verticalScroll과 연결되지 않음
QuestBehaviorWritingScreen은 scrollState를 생성하여 QuestTextField에 전달하지만, 부모 컨테이너에서 verticalScroll(scrollState)를 적용하지 않습니다. 대신 LazyColumn을 사용하고 있어서, QuestTextField 내부의 animateScrollTo 호출이 실제 스크롤 효과를 발생시키지 못합니다.
QuestRecordingScreen은 올바르게 Column에 verticalScroll(scrollState)를 적용하고 있으므로, QuestBehaviorWritingScreen도 동일한 패턴으로 수정이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt`
at line 26, QuestBehaviorWritingScreen creates and passes scrollState into
QuestTextField but never applies it to the parent so
QuestTextField.animateScrollTo has no effect; update QuestBehaviorWritingScreen
to either replace the LazyColumn with a Column and add
Modifier.verticalScroll(scrollState) or, if keeping LazyColumn, switch to a
LazyListState and adapt QuestTextField to use lazy list scrolling APIs—ensure
the symbol names referenced are QuestBehaviorWritingScreen, scrollState,
QuestTextField, verticalScroll, LazyColumn, Column, and animateScrollTo so the
parent scroll modifier matches the child scrolling calls.
| decorationBox = { innerTextField -> | ||
| if (value.isEmpty() && !(focusState.value)) { | ||
| Text( | ||
| text = placeholder, | ||
| color = ByeBooTheme.colors.gray300, | ||
| style = ByeBooTheme.typography.body3, | ||
| ) | ||
| } | ||
| innerTextField() | ||
| }, |
There was a problem hiding this comment.
decorationBox에 Box 래퍼 없음 — placeholder가 innerTextField 위에 오버레이되지 않고 수직으로 쌓임
value.isEmpty() && !focusState.value 조건이 참일 때, Text(placeholder)와 innerTextField()가 Box 없이 순차적으로 호출되면 두 컴포저블이 겹치지 않고 세로로 배치되어 플레이스홀더와 텍스트 입력 영역이 이중으로 나타납니다.
플레이스홀더 텍스트를 올바르게 표시하려면 innerTextField를 Box로 감싸고 플레이스홀더를 조건부로 표시해야 합니다.
🐛 제안 수정
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
decorationBox = { innerTextField ->
- if (value.isEmpty() && !(focusState.value)) {
- Text(
- text = placeholder,
- color = ByeBooTheme.colors.gray300,
- style = ByeBooTheme.typography.body3,
- )
- }
- innerTextField()
+ Box(modifier = Modifier.fillMaxWidth()) {
+ if (value.isEmpty() && !(focusState.value)) {
+ Text(
+ text = placeholder,
+ color = ByeBooTheme.colors.gray300,
+ style = ByeBooTheme.typography.body3,
+ )
+ }
+ innerTextField()
+ }
},📝 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.
| decorationBox = { innerTextField -> | |
| if (value.isEmpty() && !(focusState.value)) { | |
| Text( | |
| text = placeholder, | |
| color = ByeBooTheme.colors.gray300, | |
| style = ByeBooTheme.typography.body3, | |
| ) | |
| } | |
| innerTextField() | |
| }, | |
| decorationBox = { innerTextField -> | |
| Box(modifier = Modifier.fillMaxWidth()) { | |
| if (value.isEmpty() && !(focusState.value)) { | |
| Text( | |
| text = placeholder, | |
| color = ByeBooTheme.colors.gray300, | |
| style = ByeBooTheme.typography.body3, | |
| ) | |
| } | |
| innerTextField() | |
| } | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt`
around lines 60 - 69, The decorationBox currently calls Text(placeholder) and
innerTextField() sequentially causing vertical stacking; wrap innerTextField in
a Box and overlay the placeholder conditionally so the placeholder appears on
top of the text field. Modify the decorationBox lambda in QuestTextField (use
the innerTextField parameter) to place innerTextField() inside a Box and show
Text(placeholder) inside that Box only when value.isEmpty() &&
!focusState.value, ensuring placeholder uses the same sizing/alignment as the
input.
.../main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteScreen.kt
Outdated
Show resolved
Hide resolved
.../main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteScreen.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.kt (1)
12-15:⚠️ Potential issue | 🟡 Minor
createdAt기본값에 명시적ZoneId누락.
java.time.LocalDate.now()는ZoneId를 지정하지 않으면 시스템 기본 타임존에 의존합니다. 디바이스 설정에 따라 날짜 경계(예: UTC+9 사용자가 자정 전후로 퀘스트를 완료할 때)가 의도와 다를 수 있습니다. 서버와 통신하는 날짜라면ZoneId를 명시하거나 서버 측에서 날짜를 지정하는 방식을 고려해주세요.🛡️ 수정 제안
- val createdAt: String = - java.time.LocalDate - .now() - .toString(), + val createdAt: String = + java.time.LocalDate + .now(java.time.ZoneId.of("Asia/Seoul")) + .toString(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.kt` around lines 12 - 15, The default for createdAt currently uses java.time.LocalDate.now() which relies on the device's system zone; update the initialization of createdAt in QuestBehaviorCompleteState to call LocalDate.now with an explicit ZoneId (for example ZoneId.of("UTC") or a project-standard zone constant) so the date is deterministic across devices, and ensure any required imports (java.time.ZoneId) are added and any serialization expectations still match the server contract; alternatively, if dates should be server-authoritative, remove the client default and obtain createdAt from the server response instead.app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt (1)
231-252:⚠️ Potential issue | 🟡 Minor
SubcomposeAsyncImage에error상태 처리 누락.
loading은 처리하고 있지만error콜백이 없어, 이미지 로딩 실패 시 사용자에게 아무런 피드백 없이 빈 영역만 표시됩니다.🛡️ 에러 상태 처리 추가 제안
SubcomposeAsyncImage( model = ImageRequest .Builder(LocalContext.current) .data(imageUri) .memoryCachePolicy(CachePolicy.DISABLED) .diskCachePolicy(CachePolicy.DISABLED) .build(), contentDescription = "uploaded image", modifier = modifier.fillMaxSize(), contentScale = ContentScale.Crop, loading = { Box( modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } }, + error = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_cancel), + contentDescription = "image load error", + tint = ByeBooTheme.colors.gray500, + ) + } + }, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt` around lines 231 - 252, The SubcomposeAsyncImage rendering when imageUri != null lacks an error callback so failed image loads show an empty area; update the SubcomposeAsyncImage call (the one using ImageRequest.Builder and the loading lambda) to supply an error = { ... } lambda that renders a visible fallback (e.g., a Box matching modifier.fillMaxSize() with an error icon/text or a retry button) and optionally a placeholder image; ensure the error UI provides accessible contentDescription and matches the same layout/contentScale as the success case so users see feedback when loading fails.app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteViewModel.kt (1)
88-95:⚠️ Potential issue | 🟠 Major
NavigateToQuest이후ShowInAppReview미전달 가능성.
MutableSharedFlow()는 기본적으로 버퍼가 없어 구독자가 없으면emit이 무기한 suspend됩니다. 90번 줄에서NavigateToQuest가 collect되면navigateToQuest()가 즉시 호출되어 화면이 스택에서 이탈하고LaunchedEffect의 collector가 취소됩니다. 이후 93번 줄의ShowInAppReviewemit은 구독자 없이 suspend 상태가 되어 인앱 리뷰가 트리거되지 않을 수 있습니다.🐛 수정 방안 — 두 effect를 순서 보장이 필요한 경우
ShowInAppReview를 먼저 발행하거나, 별도의extraBufferCapacity를 추가하거나,Activity에서 직접 인앱 리뷰를 트리거하는 방식을 고려하세요.- } else { - viewModelScope.launch { - _sideEffect.emit(QuestBehaviorCompleteSideEffect.NavigateToQuest) - - if (uiState.value.questId == 1L) { - _sideEffect.emit(QuestBehaviorCompleteSideEffect.ShowInAppReview) - } - } - } + } else { + viewModelScope.launch { + if (uiState.value.questId == 1L) { + _sideEffect.emit(QuestBehaviorCompleteSideEffect.ShowInAppReview) + } + _sideEffect.emit(QuestBehaviorCompleteSideEffect.NavigateToQuest) + } + }또는
_sideEffect의 버퍼를 활성화:- private val _sideEffect = MutableSharedFlow<QuestBehaviorCompleteSideEffect>() + private val _sideEffect = MutableSharedFlow<QuestBehaviorCompleteSideEffect>(extraBufferCapacity = 1)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteViewModel.kt` around lines 88 - 95, The current sequence in viewModelScope.launch emits QuestBehaviorCompleteSideEffect.NavigateToQuest then conditionally emits ShowInAppReview via _sideEffect.emit, but emit can suspend if no collector (so ShowInAppReview may never be delivered after navigation cancels the collector); fix by either emitting ShowInAppReview before NavigateToQuest when uiState.value.questId == 1L, or make _sideEffect a buffered SharedFlow (e.g., configure MutableSharedFlow with extraBufferCapacity or replay) or use non-suspending emission (tryEmit) so the ShowInAppReview event is not lost—update the creation/usage of _sideEffect and the ordering in the viewModelScope.launch accordingly (referencing viewModelScope.launch, _sideEffect.emit/_sideEffect.tryEmit, and QuestBehaviorCompleteSideEffect.NavigateToQuest/ShowInAppReview).app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt (1)
117-190:⚠️ Potential issue | 🔴 Critical
selectedImageUri가 null일 때isUploading이true로 고정되는 버그Line 119에서
isUploading = true로 설정한 후, Line 122에서selectedImageUri가 null이면return@launch로 빠져나갑니다. 이 경우 Line 188의isUploading = false리셋이 실행되지 않아, 로딩 상태가 영구적으로 유지됩니다.🐛 수정 제안
val state = _uiState.value - val imageUrl = state.selectedImageUri ?: return@launch + val imageUrl = state.selectedImageUri ?: run { + _uiState.update { it.copy(isUploading = false) } + return@launch + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt` around lines 117 - 190, The uploadImage function sets _uiState.isUploading = true then returns early if selectedImageUri is null, leaving isUploading stuck true; change the flow in uploadImage (e.g., check state.selectedImageUri before setting isUploading, or wrap the upload logic in try/finally and set _uiState.update { it.copy(isUploading = false) } in finally) so that _uiState.isUploading is always reset to false when uploadImage returns (refer to uploadImage, _uiState, selectedImageUri, viewModelScope.launch).
♻️ Duplicate comments (1)
app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt (1)
67-76:decorationBox에Box래퍼가 없어 placeholder와 텍스트 필드가 세로로 쌓임이전 리뷰에서 지적된 것과 동일한 문제입니다.
placeholderText와innerTextField()가Box로 감싸지지 않아, 빈 상태에서 플레이스홀더가 입력 영역 위에 오버레이되지 않고 수직으로 배치됩니다.🐛 수정 제안
+ import androidx.compose.foundation.layout.Box + import androidx.compose.foundation.layout.fillMaxWidth decorationBox = { innerTextField -> - if (value.isEmpty() && !(isFocused.value)) { - Text( - text = placeholder, - color = ByeBooTheme.colors.gray300, - style = ByeBooTheme.typography.body3, - ) - } - innerTextField() + Box(modifier = Modifier.fillMaxWidth()) { + if (value.isEmpty() && !(isFocused.value)) { + Text( + text = placeholder, + color = ByeBooTheme.colors.gray300, + style = ByeBooTheme.typography.body3, + ) + } + innerTextField() + } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt` around lines 67 - 76, The decorationBox currently places placeholder Text and innerTextField() vertically because they aren't wrapped in a Box; update the decorationBox in QuestTextField (the lambda that references decorationBox, innerTextField, placeholder, and isFocused) to wrap both the placeholder Text and innerTextField() inside a Box (use an appropriate Modifier, e.g., fillMaxWidth/height as needed) so the placeholder overlays the text field when empty rather than stacking vertically.
🧹 Nitpick comments (8)
app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.kt (1)
19-19:selectedEmotionnullable 타입과 non-null 기본값 간 불일치.
EmotionChipType?으로 선언되어 있어null이 유효한 상태처럼 보이지만, 기본값은EmotionChipType.EMOTION_NEUTRAL(non-null)입니다. "선택 없음" 상태를null로 표현하려는 의도라면 기본값도null이 더 명확하고, 그렇지 않다면?를 제거하는 것이 적절합니다.♻️ 리팩토링 제안 — "선택 없음"을 null로 표현하는 경우
- val selectedEmotion: EmotionChipType? = EmotionChipType.EMOTION_NEUTRAL, + val selectedEmotion: EmotionChipType? = null,♻️ 리팩토링 제안 — null 상태가 불필요한 경우
- val selectedEmotion: EmotionChipType? = EmotionChipType.EMOTION_NEUTRAL, + val selectedEmotion: EmotionChipType = EmotionChipType.EMOTION_NEUTRAL,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.kt` at line 19, The declaration selectedEmotion: EmotionChipType? = EmotionChipType.EMOTION_NEUTRAL is inconsistent (nullable type with a non-null default); choose one: either make it non-null by changing the type to EmotionChipType (remove the '?') to keep the default EmotionChipType.EMOTION_NEUTRAL, or make the default explicitly null (selectedEmotion: EmotionChipType? = null) if you intend to represent “no selection” as null; update any usages of selectedEmotion in QuestBehaviorCompleteState and related code to handle the chosen nullability accordingly.app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt (1)
131-131: 외부에서 전달된modifier를 내부 컴포저블에 반복 사용하는 패턴 개선 권장.Compose 컨벤션상
modifier파라미터는 루트 컴포저블(여기서는 110번 줄Column)에만 적용해야 합니다.Icon(131번 줄),Spacer(135번 줄),LazyColumn(138번 줄) 등 내부 컴포저블에는Modifier(새 인스턴스) 또는 지역 변수를 사용해야 합니다. 현재는 호출부에서 기본값(Modifier)을 전달하므로 즉각적인 문제는 없지만, 호출부에서 레이아웃 제약이 포함된 modifier를 전달할 경우 예기치 않은 레이아웃이 발생할 수 있습니다.♻️ 내부 컴포저블 modifier 수정 예시
- modifier = modifier.clickable(onClick = onCloseClick), + modifier = Modifier.clickable(onClick = onCloseClick),- Spacer(modifier = modifier.height(screenHeightDp(16.dp))) + Spacer(modifier = Modifier.height(screenHeightDp(16.dp)))- modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(),이하 파일 전체의 내부 컴포저블
modifier = modifier.*패턴을 동일하게modifier = Modifier.*로 교체하는 것을 권장합니다.Also applies to: 135-135, 138-138
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt` at line 131, The passed-in parameter modifier is being reused on internal composables (e.g., Icon, Spacer, LazyColumn) which can leak external layout constraints; change those internal usages to new Modifier instances or local modifiers instead: apply the incoming modifier only to the root Column (Column at top) and replace occurrences like modifier = modifier.clickable(...) / modifier = modifier.fillMaxWidth() inside Icon, Spacer, LazyColumn with Modifier.clickable(...), Modifier.fillMaxWidth(), etc.; update any helper local variables if needed and keep the function parameter name 'modifier' applied solely to the root composable.app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt (2)
159-159:BringIntoViewRequester가 생성만 되고 사용되지 않음
bringIntoViewRequester가remember로 생성되었지만,Modifier.bringIntoViewRequester()에 연결되거나.bringIntoView()가 호출되는 곳이 없습니다. 불필요하면 제거하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt` at line 159, bringIntoViewRequester is created via remember { BringIntoViewRequester() } but never used; either remove this unused variable or attach it to a composable modifier via Modifier.bringIntoViewRequester(bringIntoViewRequester) and/or call bringIntoViewRequester.bringIntoView() where needed (e.g., in response to focus/interaction) so the BringIntoViewRequester is actually applied; update references in QuestBehaviorWritingScreen.kt around the bringIntoViewRequester declaration and the relevant composable that should scroll into view.
353-365:advancedImePadding은composed {}대신Modifier.Node기반 구현을 고려
composed {}는 각 사용처마다 새로운 컴포지션을 생성하여 성능 오버헤드가 있습니다. Compose 최신 권장 사항에서는Modifier.NodeAPI를 사용하는 것을 권장합니다. 현재 단일 사용이므로 즉각적인 문제는 아니지만, 참고해 두시면 좋겠습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt` around lines 353 - 365, The current advancedImePadding() uses composed { } which creates a new composition per usage; replace it with a Modifier.Node-based implementation: create a custom Modifier.Node (e.g., AdvancedImePaddingNode) that holds the mutable state (consumePadding) and overrides onAttach/onDetach and onGloballyPositioned handling (or implements MeasurePolicy/placement as appropriate) to compute consumePadding from findRootCoordinates(), positionInWindow(), and size, then expose a Modifier that applies the computed bottom inset via consumeWindowInsets(PaddingValues(bottom = with(LocalDensity.current) { consumePadding.toDp() })) and chains imePadding(); update advancedImePadding() to return this node-based modifier instead of using composed { } and keep the references to consumePadding, onGloballyPositioned logic, consumeWindowInsets and imePadding intact.app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt (2)
57-69:buildAnnotatedString대신 단순 문자열 보간 사용 가능스타일링 없이
append만 호출하고 있으므로,"$currentCharCount/$maxCharCount"로 충분합니다.♻️ 간소화 제안
Text( - text = - buildAnnotatedString { - append(text = currentCharCount.toString()) - - append(text = "/") - - append(text = maxCharCount.toString()) - }, + text = "$currentCharCount/$maxCharCount", style = ByeBooTheme.typography.body6, color = ByeBooTheme.colors.gray400, textAlign = TextAlign.End, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt` around lines 57 - 69, In QuestWritingFooter's Text composable, replace the buildAnnotatedString block (which only appends values) with a simple string interpolation for the text property; e.g., set text = "$currentCharCount/$maxCharCount" inside the Text call (referencing currentCharCount and maxCharCount) to remove the unnecessary buildAnnotatedString usage and simplify the UI code.
29-29: 최대 글자 수200이 여러 곳에 하드코딩되어 있음
maxCharCount가 여기서는200으로,QuestBehaviorWritingScreen.ktLine 340에서도it.length <= 200으로 중복 정의되어 있습니다. 값이 불일치하면 카운터 표시와 실제 제한이 다를 수 있으니, 상수로 추출하여 단일 소스에서 관리하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt` at line 29, Extract the duplicated magic number 200 into a single constant and use it everywhere to avoid mismatched limits: create a public constant (e.g., MAX_PHOTO_QUESTION_CHAR_COUNT or PHOTO_QUESTION_MAX_CHARS) and replace the inline literal in QuestWritingFooter.kt (val maxCharCount) and the check in QuestBehaviorWritingScreen.kt (the it.length <= 200 condition) to reference that constant; ensure the constant is placed in a shared file or companion object accessible from both classes so the UI counter and validation use the same source of truth.app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt (1)
43-56: 외부Box와ImageUploadButton의 크기/클리핑이 중복됨
QuestPhotoPicker의 외부Box(Line 43-56)와 내부ImageUploadButton(Line 66-72) 모두.width(screenWidthDp(312.dp)).aspectRatio(1f).clip(RoundedCornerShape(12.dp))를 적용하고 있습니다. 외부Box에서 크기와 클리핑을 담당하고 내부에서는fillMaxSize()를 사용하거나, 외부 래퍼를 제거하는 것이 더 깔끔합니다.Also applies to: 66-72
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt` around lines 43 - 56, Duplicate sizing and clipping are applied to both the outer Box in QuestPhotoPicker and the inner ImageUploadButton; pick one place to own size/clip. Fix by keeping the outer Box (QuestPhotoPicker) with .width(screenWidthDp(312.dp)).aspectRatio(1f).clip(RoundedCornerShape(12.dp)) and remove width/aspectRatio/clip from the ImageUploadButton invocation so ImageUploadButton uses Modifier.fillMaxSize() (or otherwise only fills its parent) and relies on the Box for clipping; alternatively, if you prefer the child to own layout, remove the outer Box and apply the size/clip to ImageUploadButton—update the ImageUploadButton call site (and its modifier usage) accordingly so sizing/clipping is not duplicated.app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt (1)
34-34:lastLineBottom이 계산만 되고 사용되지 않음
lastLineBottom은onTextLayout에서 값이 설정되지만, 이 컴포저블 내에서도 외부에서도 참조되지 않습니다. 이전 구현에서 남은 데드 코드로 보이며, 제거해도 됩니다.♻️ 데드 코드 제거 제안
val isFocused = remember { mutableStateOf(false) } - val lastLineBottom = remember { mutableStateOf(0) } ... - onTextLayout = { layoutResult -> - lastLineBottom.value = - layoutResult.getLineBottom(layoutResult.lineCount - 1).toInt() - }, )Also applies to: 77-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt` at line 34, Remove the dead state `lastLineBottom` from the QuestTextField composable: it is created via `remember { mutableStateOf(0) }` and only ever set in the `onTextLayout` callback but never read anywhere; delete the `lastLineBottom` declaration and any assignments to it inside `onTextLayout` (also remove the similar dead code around lines 77-80), ensuring no remaining references to `lastLineBottom` remain in the `QuestTextField` function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt`:
- Line 227: Replace the incorrect use of the incoming composable parameter
"modifier" on internal Spacer calls with the global Compose Modifier to avoid
propagating caller modifiers: in QuestBehaviorWritingScreen.kt update the Spacer
at the top-level and all Spacers inside EssentialSection and OptionalSection
(the usages flagged around lines 227, 278, 286, 295, 326, 335) to use
Modifier.height(...) (or Modifier.size/Modifier.padding as appropriate) instead
of modifier.height(...), ensuring only the root composable retains the
"modifier" parameter.
- Line 3: Add the missing package declaration to QuestPhotoPicker.kt by
inserting "package com.byeboo.app.presentation.quest.behavior.component" at the
top and change the bare import in QuestBehaviorWritingScreen.kt to import
com.byeboo.app.presentation.quest.behavior.component.QuestPhotoPicker; replace
all uses of the passed-in modifier on internal Spacer composables with Modifier
(e.g. modifier.height(...) → Modifier.height(...), modifier.width(...) →
Modifier.width(...)) so the composable's modifier only affects the root element;
remove the unused bringIntoViewRequester variable and its import since
bringIntoViewRequester is never referenced; and review the duplicated state
update where onUpdateSelectedImage(uiState.selectedImageUri) is called
immediately after onCompleteClick(context) — either remove the redundant
onUpdateSelectedImage call or adjust logic so state isn't updated twice
(functions to check: onCompleteClick and onUpdateSelectedImage).
---
Outside diff comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt`:
- Around line 231-252: The SubcomposeAsyncImage rendering when imageUri != null
lacks an error callback so failed image loads show an empty area; update the
SubcomposeAsyncImage call (the one using ImageRequest.Builder and the loading
lambda) to supply an error = { ... } lambda that renders a visible fallback
(e.g., a Box matching modifier.fillMaxSize() with an error icon/text or a retry
button) and optionally a placeholder image; ensure the error UI provides
accessible contentDescription and matches the same layout/contentScale as the
success case so users see feedback when loading fails.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.kt`:
- Around line 12-15: The default for createdAt currently uses
java.time.LocalDate.now() which relies on the device's system zone; update the
initialization of createdAt in QuestBehaviorCompleteState to call LocalDate.now
with an explicit ZoneId (for example ZoneId.of("UTC") or a project-standard zone
constant) so the date is deterministic across devices, and ensure any required
imports (java.time.ZoneId) are added and any serialization expectations still
match the server contract; alternatively, if dates should be
server-authoritative, remove the client default and obtain createdAt from the
server response instead.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteViewModel.kt`:
- Around line 88-95: The current sequence in viewModelScope.launch emits
QuestBehaviorCompleteSideEffect.NavigateToQuest then conditionally emits
ShowInAppReview via _sideEffect.emit, but emit can suspend if no collector (so
ShowInAppReview may never be delivered after navigation cancels the collector);
fix by either emitting ShowInAppReview before NavigateToQuest when
uiState.value.questId == 1L, or make _sideEffect a buffered SharedFlow (e.g.,
configure MutableSharedFlow with extraBufferCapacity or replay) or use
non-suspending emission (tryEmit) so the ShowInAppReview event is not
lost—update the creation/usage of _sideEffect and the ordering in the
viewModelScope.launch accordingly (referencing viewModelScope.launch,
_sideEffect.emit/_sideEffect.tryEmit, and
QuestBehaviorCompleteSideEffect.NavigateToQuest/ShowInAppReview).
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt`:
- Around line 117-190: The uploadImage function sets _uiState.isUploading = true
then returns early if selectedImageUri is null, leaving isUploading stuck true;
change the flow in uploadImage (e.g., check state.selectedImageUri before
setting isUploading, or wrap the upload logic in try/finally and set
_uiState.update { it.copy(isUploading = false) } in finally) so that
_uiState.isUploading is always reset to false when uploadImage returns (refer to
uploadImage, _uiState, selectedImageUri, viewModelScope.launch).
---
Duplicate comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt`:
- Around line 67-76: The decorationBox currently places placeholder Text and
innerTextField() vertically because they aren't wrapped in a Box; update the
decorationBox in QuestTextField (the lambda that references decorationBox,
innerTextField, placeholder, and isFocused) to wrap both the placeholder Text
and innerTextField() inside a Box (use an appropriate Modifier, e.g.,
fillMaxWidth/height as needed) so the placeholder overlays the text field when
empty rather than stacking vertically.
---
Nitpick comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt`:
- Line 131: The passed-in parameter modifier is being reused on internal
composables (e.g., Icon, Spacer, LazyColumn) which can leak external layout
constraints; change those internal usages to new Modifier instances or local
modifiers instead: apply the incoming modifier only to the root Column (Column
at top) and replace occurrences like modifier = modifier.clickable(...) /
modifier = modifier.fillMaxWidth() inside Icon, Spacer, LazyColumn with
Modifier.clickable(...), Modifier.fillMaxWidth(), etc.; update any helper local
variables if needed and keep the function parameter name 'modifier' applied
solely to the root composable.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.kt`:
- Line 19: The declaration selectedEmotion: EmotionChipType? =
EmotionChipType.EMOTION_NEUTRAL is inconsistent (nullable type with a non-null
default); choose one: either make it non-null by changing the type to
EmotionChipType (remove the '?') to keep the default
EmotionChipType.EMOTION_NEUTRAL, or make the default explicitly null
(selectedEmotion: EmotionChipType? = null) if you intend to represent “no
selection” as null; update any usages of selectedEmotion in
QuestBehaviorCompleteState and related code to handle the chosen nullability
accordingly.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt`:
- Around line 43-56: Duplicate sizing and clipping are applied to both the outer
Box in QuestPhotoPicker and the inner ImageUploadButton; pick one place to own
size/clip. Fix by keeping the outer Box (QuestPhotoPicker) with
.width(screenWidthDp(312.dp)).aspectRatio(1f).clip(RoundedCornerShape(12.dp))
and remove width/aspectRatio/clip from the ImageUploadButton invocation so
ImageUploadButton uses Modifier.fillMaxSize() (or otherwise only fills its
parent) and relies on the Box for clipping; alternatively, if you prefer the
child to own layout, remove the outer Box and apply the size/clip to
ImageUploadButton—update the ImageUploadButton call site (and its modifier
usage) accordingly so sizing/clipping is not duplicated.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt`:
- Line 159: bringIntoViewRequester is created via remember {
BringIntoViewRequester() } but never used; either remove this unused variable or
attach it to a composable modifier via
Modifier.bringIntoViewRequester(bringIntoViewRequester) and/or call
bringIntoViewRequester.bringIntoView() where needed (e.g., in response to
focus/interaction) so the BringIntoViewRequester is actually applied; update
references in QuestBehaviorWritingScreen.kt around the bringIntoViewRequester
declaration and the relevant composable that should scroll into view.
- Around line 353-365: The current advancedImePadding() uses composed { } which
creates a new composition per usage; replace it with a Modifier.Node-based
implementation: create a custom Modifier.Node (e.g., AdvancedImePaddingNode)
that holds the mutable state (consumePadding) and overrides onAttach/onDetach
and onGloballyPositioned handling (or implements MeasurePolicy/placement as
appropriate) to compute consumePadding from findRootCoordinates(),
positionInWindow(), and size, then expose a Modifier that applies the computed
bottom inset via consumeWindowInsets(PaddingValues(bottom =
with(LocalDensity.current) { consumePadding.toDp() })) and chains imePadding();
update advancedImePadding() to return this node-based modifier instead of using
composed { } and keep the references to consumePadding, onGloballyPositioned
logic, consumeWindowInsets and imePadding intact.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt`:
- Around line 57-69: In QuestWritingFooter's Text composable, replace the
buildAnnotatedString block (which only appends values) with a simple string
interpolation for the text property; e.g., set text =
"$currentCharCount/$maxCharCount" inside the Text call (referencing
currentCharCount and maxCharCount) to remove the unnecessary
buildAnnotatedString usage and simplify the UI code.
- Line 29: Extract the duplicated magic number 200 into a single constant and
use it everywhere to avoid mismatched limits: create a public constant (e.g.,
MAX_PHOTO_QUESTION_CHAR_COUNT or PHOTO_QUESTION_MAX_CHARS) and replace the
inline literal in QuestWritingFooter.kt (val maxCharCount) and the check in
QuestBehaviorWritingScreen.kt (the it.length <= 200 condition) to reference that
constant; ensure the constant is placed in a shared file or companion object
accessible from both classes so the UI counter and validation use the same
source of truth.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt`:
- Line 34: Remove the dead state `lastLineBottom` from the QuestTextField
composable: it is created via `remember { mutableStateOf(0) }` and only ever set
in the `onTextLayout` callback but never read anywhere; delete the
`lastLineBottom` declaration and any assignments to it inside `onTextLayout`
(also remove the similar dead code around lines 77-80), ensuring no remaining
references to `lastLineBottom` remain in the `QuestTextField` function.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
app/src/main/java/com/byeboo/app/presentation/quest/behavior/QuestBehaviorWritingScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/navigation/QuestBehaviorNavigation.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorState.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt
💤 Files with no reviewable changes (1)
- app/src/main/java/com/byeboo/app/presentation/quest/behavior/QuestBehaviorWritingScreen.kt
✅ Files skipped from review due to trivial changes (1)
- app/src/main/java/com/byeboo/app/presentation/quest/behavior/navigation/QuestBehaviorNavigation.kt
| @@ -0,0 +1,367 @@ | |||
| package com.byeboo.app.presentation.quest.behavior.writing | |||
|
|
|||
| import QuestPhotoPicker | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "QuestPhotoPicker.kt" -type f -exec head -20 {} +Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1011
🏁 Script executed:
find . -name "QuestBehaviorWritingScreen.kt" -type f -exec wc -l {} +Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 179
🏁 Script executed:
fd "QuestBehaviorWritingScreen.kt" -x cat -n | head -100Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 5431
🏁 Script executed:
# QuestPhotoPicker.kt 위치와 첫 5줄 확인
find . -name "QuestPhotoPicker.kt" -type f -exec echo "File: {}" \; -exec head -5 {} \;Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 424
🏁 Script executed:
# QuestBehaviorWritingScreen.kt에서 BringIntoViewRequester 사용 여부 확인
grep -n "BringIntoViewRequester" ./app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 221
🏁 Script executed:
# QuestBehaviorWritingScreen.kt에서 Spacer와 modifier 사용 확인
grep -n "Spacer" ./app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt | head -20Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 852
🏁 Script executed:
# onUpdateSelectedImage 관련 코드 확인
grep -n "onUpdateSelectedImage" ./app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 422
🏁 Script executed:
# Line 185-200 문맥 확인 - onCompleteClick 핸들러
sed -n '185,200p' ./app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 519
🏁 Script executed:
# Line 260-305 문맥 확인 - EssentialSection과 OptionalSection 정의
sed -n '260,305p' ./app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1390
🏁 Script executed:
# Line 320-345 문맥 확인 - OptionalSection의 Spacer 사용
sed -n '320,345p' ./app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 884
🏁 Script executed:
# BringIntoViewRequester 전체 파일에서 진짜 사용되는지 최종 확인
rg "bringIntoViewRequester" ./app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 145
QuestPhotoPicker.kt에 패키지 선언 누락 — 수정 필요
import QuestPhotoPicker의 bare import는 QuestPhotoPicker.kt 파일에 패키지 선언이 없기 때문입니다. 파일이 com.byeboo.app.presentation.quest.behavior.component 디렉토리에 위치하지만 package 선언이 누락되어 있습니다. 파일 시작에 package com.byeboo.app.presentation.quest.behavior.component를 추가하고 import를 import com.byeboo.app.presentation.quest.behavior.component.QuestPhotoPicker로 수정하세요.
Spacer의 modifier 파라미터 오용 (6곳) — 리팩토링 필요
Composable의 modifier 파라미터는 최상위 요소에만 적용되어야 합니다. 다음 라인들에서 내부 Spacer에 modifier를 적용하고 있습니다:
- 227, 278, 286, 295, 326, 335번 라인
modifier.height() → Modifier.height(), modifier.width() → Modifier.width()로 변경하세요.
BringIntoViewRequester 미사용 — 정리 필요
159번 라인에서 생성된 bringIntoViewRequester가 파일 전체에서 사용되지 않습니다. 변수와 21번 라인의 import를 제거하세요.
onUpdateSelectedImage 중복 호출 — 검토 필요
192번 라인에서 onCompleteClick(context) 직후 onUpdateSelectedImage(uiState.selectedImageUri)를 호출하는데, 현재 상태값으로 업데이트하는 것으로 보입니다. 의도를 재검토하거나 필요 없으면 제거하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt`
at line 3, Add the missing package declaration to QuestPhotoPicker.kt by
inserting "package com.byeboo.app.presentation.quest.behavior.component" at the
top and change the bare import in QuestBehaviorWritingScreen.kt to import
com.byeboo.app.presentation.quest.behavior.component.QuestPhotoPicker; replace
all uses of the passed-in modifier on internal Spacer composables with Modifier
(e.g. modifier.height(...) → Modifier.height(...), modifier.width(...) →
Modifier.width(...)) so the composable's modifier only affects the root element;
remove the unused bringIntoViewRequester variable and its import since
bringIntoViewRequester is never referenced; and review the duplicated state
update where onUpdateSelectedImage(uiState.selectedImageUri) is called
immediately after onCompleteClick(context) — either remove the redundant
onUpdateSelectedImage call or adjust logic so state isn't updated twice
(functions to check: onCompleteClick and onUpdateSelectedImage).
...c/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (3)
app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt (3)
3-3:import QuestPhotoPicker— 패키지 경로 없는 bare import (미해결)
QuestPhotoPicker.kt에 패키지 선언이 누락되어 있어 bare import가 사용되고 있습니다.QuestPhotoPicker.kt상단에package com.byeboo.app.presentation.quest.behavior.component를 추가하고, 이 import를import com.byeboo.app.presentation.quest.behavior.component.QuestPhotoPicker로 수정하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt` at line 3, The import for QuestPhotoPicker is a bare unresolved import; open QuestPhotoPicker.kt and add the package declaration "package com.byeboo.app.presentation.quest.behavior.component" at the top, then in QuestBehaviorWritingScreen.kt change the import to import com.byeboo.app.presentation.quest.behavior.component.QuestPhotoPicker so the symbol QuestPhotoPicker resolves correctly.
225-225: 내부Spacer에 외부modifier파라미터 사용 (미해결)Lines 225, 274, 282, 291, 322, 331 모두 내부
Spacer에 상위 컴포저블로 전달받은modifier를 체이닝하고 있습니다. Compose 컨벤션상modifier파라미터는 루트 컴포저블에만 적용해야 하며, 내부 요소에는Modifier(전역 인스턴스)를 사용해야 합니다.-Spacer(modifier = modifier.height(screenHeightDp(20.dp))) // line 225 +Spacer(modifier = Modifier.height(screenHeightDp(20.dp)))
EssentialSection(Lines 274, 282, 291) 및OptionalSection(Lines 322, 331)의 모든 내부Spacer도 동일하게modifier→Modifier로 변경하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt` at line 225, The internal Spacer calls are incorrectly chaining the parent composable's modifier parameter; change those to use the global Modifier instance instead. Locate all Spacer usages inside QuestBehaviorWritingScreen, EssentialSection, and OptionalSection that currently do things like Spacer(modifier = modifier.height(...)) and replace them with Spacer(modifier = Modifier.height(...)); ensure only the root composable continues to accept and apply the passed-in modifier parameter while all inner Spacers use Modifier.
188-192:onCompleteClick직후 현재 상태값으로onUpdateSelectedImage재호출 — 의도 불명확 (미해결)
onCompleteClick(context)직후onUpdateSelectedImage(uiState.selectedImageUri)를 현재 상태값 그대로 호출하는 이유가 불명확합니다.onCompleteClick내부에서 이미 처리되는 동작과 중복될 수 있습니다. 의도를 재확인하거나 불필요하면 제거하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt` around lines 188 - 192, The call to onUpdateSelectedImage(uiState.selectedImageUri) immediately after invoking onCompleteClick(context) is redundant or unclear; either remove the extra call, or modify onCompleteClick to return/emit the updated image URI and call onUpdateSelectedImage with that returned value (instead of reusing uiState.selectedImageUri), or move the update logic into onCompleteClick so a single code path handles image updates; look for the lambda where onCompleteClick and onUpdateSelectedImage are invoked and update the behavior of onCompleteClick/onUpdateSelectedImage (or the caller) accordingly to avoid duplicate updates.
🧹 Nitpick comments (2)
app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt (1)
154-154:isFocused상태는 쓰기만 하고 읽지 않음 — 데드 스테이트
isFocused는 Line 172의 키 인터셉터와 Line 229의onFocusChanged콜백에서 갱신되지만,QuestBehaviorWritingScreen어디에서도 읽히지 않습니다. 레이아웃 분기는isImeVisible로만 결정됩니다.
isFocused선언, Line 172의isFocused.value = false대입, Line 229의onFocusChanged = { isFocused.value = it }콜백을 모두 제거하세요.OptionalSection의onFocusChanged파라미터도 해당 호출자에서 제거할 수 있습니다.♻️ 수정 제안
-val isFocused = remember { mutableStateOf(false) }if (event.key.nativeKeyCode == KeyEvent.KEYCODE_BACK) { focusManager.clearFocus(force = true) - isFocused.value = false trueOptionalSection( questAnswer = uiState.questAnswer, - onFocusChanged = { isFocused.value = it }, onUpdateContent = onUpdateContent, scrollState = scrollState )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt` at line 154, Remove the dead isFocused state and its related updates: delete the remember { mutableStateOf(false) } declaration named isFocused in QuestBehaviorWritingScreen, remove the assignment isFocused.value = false in the key interceptor, and remove the onFocusChanged = { isFocused.value = it } callback usage; also remove the corresponding onFocusChanged parameter from OptionalSection where it's no longer used. Ensure no other code references isFocused remain in QuestBehaviorWritingScreen.app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt (1)
29-29:Key.Back사용으로 Compose 관용 코드 개선 권장현재 코드가
event.key.nativeKeyCode == KeyEvent.KEYCODE_BACK방식으로 Back 키를 비교하고 있습니다. Compose의 관용적 방식인Key.Back을 사용하는 것이 권장됩니다. 이는 Android View 레이어의 상수에 직접 의존하지 않고 더 깔끔한 코드입니다.♻️ `Key.Back` 관용 코드로 교체
+import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.nativeKeyCode ... .onPreInterceptKeyBeforeSoftKeyboard { event -> - if (event.key.nativeKeyCode == KeyEvent.KEYCODE_BACK) { + if (event.key == Key.Back) { focusManager.clearFocus(force = true) isFocused.value = false true } else { false } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt` at line 29, Replace the Android KeyEvent numeric comparison with Compose's Key.Back: in QuestRecordingScreen (the composable that uses onPreInterceptKeyBeforeSoftKeyboard) locate the check using event.key.nativeKeyCode == KeyEvent.KEYCODE_BACK and change it to compare event.key == Key.Back (and preserve any existing event.type/KeyEventType checks you have, e.g., only handle KeyUp if that was intended) so the code uses Compose's idiomatic Key.Back instead of the platform constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt`:
- Around line 161-162: The code reads WindowInsets.ime.getBottom(density)
directly in composition (val density = LocalDensity.current; val isImeVisible =
WindowInsets.ime.getBottom(density) > 0) which can be one frame late; wrap the
read in a remembered derived state (e.g., remember { derivedStateOf {
WindowInsets.ime.getBottom(density) > 0 } }) so the IME inset is tracked
correctly, or better yet remove manual reading and apply Modifier.imePadding() /
Modifier.windowInsetsPadding(WindowInsets.ime) on the composable to let the
layout stage handle insets.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.kt`:
- Around line 32-47: The questCommonGraph currently only registers
composable<QuestCommon> but the navigation callback
navigateToQuestCommonComplete expects a destination
composable<QuestCommonComplete>; add a composable<QuestCommonComplete>
registration inside questCommonGraph that maps to the QuestCommonComplete screen
and uses the questId argument so navigateToQuestCommonComplete(questId) resolves
at runtime; ensure the routeNavigation block includes both
composable<QuestCommon> (which calls QuestCommonRoute) and a new
composable<QuestCommonComplete> entry that instantiates the corresponding
screen/component and reads the Long questId parameter.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.kt`:
- Around line 38-56: QuestCommonRoute currently doesn't observe ViewModel side
effects and never calls the passed navigation lambdas; add a LaunchedEffect in
QuestCommonRoute to collect viewModel.sideEffect (e.g., collectLatest) and map
emitted QuestCommonSideEffect cases to call navigateToQuest,
navigateToQuestCommonComplete(id) and navigateUp accordingly; also update the
ViewModel methods onBackClicked() and onCompleteClicked() to emit the new side
effects (NavigateUp and NavigateToQuestCommonComplete with the quest id) and
extend the QuestCommonSideEffect sealed class to include
NavigateToQuestCommonComplete(Long) and NavigateUp so the Route's collector can
dispatch the passed callbacks.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.kt`:
- Around line 24-30: QuestCommonViewModel currently has no-op handlers
(onBackClicked/onCompleteClicked) so UI clicks are ignored; implement these to
either emit navigation sideEffects or remove the handlers and let the UI handle
navigation. Inside QuestCommonViewModel, update onBackClicked to emit a
sideEffect event (e.g., NavigateBack) via your existing side-effect channel (the
same mechanism used elsewhere in this VM), and update onCompleteClicked to emit
a Complete/Finish sideEffect (or a ConfirmComplete event) that the UI observes;
if you prefer not to emit from the ViewModel, delete these empty methods and
wire the UI to perform navigation directly instead. Ensure you reference the
existing side-effect flow/channel used in this class to keep behavior consistent
and test that the Fragment/Activity reacts to the new events.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt`:
- Around line 221-233: The onValueChange currently ignores any input when
it.length > 500, causing pasted text >500 chars to be dropped; change the
handler inside QuestTextField so it always calls
onUpdateContent(isFocused.value, newValueTrimmed) but trims the incoming text to
500 characters (e.g., val v = if (it.length <= 500) it else it.take(500)) and
pass v to onUpdateContent; update references in the QuestTextField onValueChange
block and ensure uiState.questAnswer continues to reflect the trimmed value so
the UI and clipboard UX match.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/screen/CommonJourneyScreen.kt`:
- Around line 145-146: The click handler is hardcoding questId as 0
(onCommonQuestClick(0)), causing wrong navigation; change it to pass the real
quest identifier from the UI state/props (e.g., use the Quest item's id or the
screen state variable) so the correct quest is opened. Locate the onClick
invocation that calls onCommonQuestClick(0) in CommonJourneyScreen and replace
the literal 0 with the actual questId source (for example item.id, quest.id, or
a questId property supplied to the composable); if that property isn't
available, thread the questId through the composable parameters or flow and
update the caller to provide it. Ensure onCommonQuestClick is still used for
navigation but receives the real identifier.
---
Duplicate comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt`:
- Line 3: The import for QuestPhotoPicker is a bare unresolved import; open
QuestPhotoPicker.kt and add the package declaration "package
com.byeboo.app.presentation.quest.behavior.component" at the top, then in
QuestBehaviorWritingScreen.kt change the import to import
com.byeboo.app.presentation.quest.behavior.component.QuestPhotoPicker so the
symbol QuestPhotoPicker resolves correctly.
- Line 225: The internal Spacer calls are incorrectly chaining the parent
composable's modifier parameter; change those to use the global Modifier
instance instead. Locate all Spacer usages inside QuestBehaviorWritingScreen,
EssentialSection, and OptionalSection that currently do things like
Spacer(modifier = modifier.height(...)) and replace them with Spacer(modifier =
Modifier.height(...)); ensure only the root composable continues to accept and
apply the passed-in modifier parameter while all inner Spacers use Modifier.
- Around line 188-192: The call to
onUpdateSelectedImage(uiState.selectedImageUri) immediately after invoking
onCompleteClick(context) is redundant or unclear; either remove the extra call,
or modify onCompleteClick to return/emit the updated image URI and call
onUpdateSelectedImage with that returned value (instead of reusing
uiState.selectedImageUri), or move the update logic into onCompleteClick so a
single code path handles image updates; look for the lambda where
onCompleteClick and onUpdateSelectedImage are invoked and update the behavior of
onCompleteClick/onUpdateSelectedImage (or the caller) accordingly to avoid
duplicate updates.
---
Nitpick comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt`:
- Line 154: Remove the dead isFocused state and its related updates: delete the
remember { mutableStateOf(false) } declaration named isFocused in
QuestBehaviorWritingScreen, remove the assignment isFocused.value = false in the
key interceptor, and remove the onFocusChanged = { isFocused.value = it }
callback usage; also remove the corresponding onFocusChanged parameter from
OptionalSection where it's no longer used. Ensure no other code references
isFocused remain in QuestBehaviorWritingScreen.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt`:
- Line 29: Replace the Android KeyEvent numeric comparison with Compose's
Key.Back: in QuestRecordingScreen (the composable that uses
onPreInterceptKeyBeforeSoftKeyboard) locate the check using
event.key.nativeKeyCode == KeyEvent.KEYCODE_BACK and change it to compare
event.key == Key.Back (and preserve any existing event.type/KeyEventType checks
you have, e.g., only handle KeyUp if that was intended) so the code uses
Compose's idiomatic Key.Back instead of the platform constant.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.ktapp/src/main/java/com/byeboo/app/presentation/main/MainNavigator.ktapp/src/main/java/com/byeboo/app/presentation/quest/QuestScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonRoute.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonState.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingTitle.ktapp/src/main/java/com/byeboo/app/presentation/quest/model/QuestState.ktapp/src/main/java/com/byeboo/app/presentation/quest/navigation/QuestNavigation.ktapp/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/screen/CommonJourneyScreen.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingTitle.kt
| val density = LocalDensity.current | ||
| val isImeVisible = WindowInsets.ime.getBottom(density) > 0 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, examine the file and context around lines 161-162
if [ -f "app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt" ]; then
echo "=== File context (lines 150-175) ==="
sed -n '150,175p' "app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt"
else
echo "File not found at expected path, searching..."
find . -name "QuestBehaviorWritingScreen.kt" -type f
fiRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1055
🏁 Script executed:
# Search for WindowInsets usage patterns in the codebase
echo "=== WindowInsets usage in codebase ==="
rg "WindowInsets\." -A 2 -B 2 --type kotlin | head -100Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1341
🏁 Script executed:
# Search for derivedStateOf usage patterns
echo "=== derivedStateOf usage patterns ==="
rg "derivedStateOf" -A 2 -B 2 --type kotlin | head -80Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1310
🌐 Web query:
Jetpack Compose WindowInsets IME timing composition phase layout phase
💡 Result:
In Compose, WindowInsets (including WindowInsets.ime) are updated after the composition phase but before the layout phase. So if you read IME insets during composition (e.g., WindowInsets.ime.getBottom(LocalDensity.current) inside a @Composable), you’ll typically get a value that’s one frame late, which looks like “IME timing is off.” [1]
Because IME insets can change every frame during the keyboard animation, reading them in composition can also cause recomposition every frame; the insets API is explicitly designed so you should avoid reading inset values in composition and instead rely on the provided modifiers that defer the read to layout, keeping updates in the same frame. [1][2][3]
Practical guidance:
- Prefer layout-phase inset modifiers like
Modifier.imePadding()/Modifier.windowInsetsPadding(WindowInsets.ime)/Modifier.windowInsetsBottomHeight(WindowInsets.ime)rather than manually readinggetBottom()in composition. [1][3] - If you truly need the raw pixel value, structure it so it’s read in layout/placement (not composition), consistent with Compose’s phase/state-read model. [2]
Sources:
[1] https://developer.android.com/develop/ui/compose/system/insets-ui
[2] https://developer.android.com/develop/ui/compose/phases
[3] https://composables.com/docs/androidx.compose.foundation/foundation-layout/1.9.0-beta02/interfaces/WindowInsets
🌐 Web query:
Compose derivedStateOf WindowInsets IME insets best practice
💡 Result:
Best practice: don’t manually plumb IME insets through state unless you have a specific need. Prefer insets-aware modifiers so layout reacts (and animates) correctly as the IME moves.
1) Prefer Modifier.imePadding() / windowInsetsPadding(WindowInsets.ime)
This keeps the IME handling in layout where it belongs (and Compose will animate inset changes). (developer.android.com)
Column(
Modifier
.fillMaxSize()
.imePadding() // or .windowInsetsPadding(WindowInsets.ime)
) { /* content */ }2) If you must derive a value (visibility/height), use remember { derivedStateOf { … } }
This prevents extra recompositions when the raw inset value changes frequently but your derived result doesn’t (general Compose perf guidance). (developer.android.com)
`@Composable`
fun rememberImeBottomDp(): State<Dp> {
val density = LocalDensity.current
return remember(density) {
derivedStateOf {
with(density) { WindowInsets.ime.getBottom(this).toDp() }
}
}
}For a boolean:
val isImeVisible by remember {
derivedStateOf { WindowInsets.ime.getBottom(LocalDensity.current) > 0 }
}(You’ll commonly see this pattern recommended for “keyboard open/close” detection.) (stackoverflow.com)
3) Make sure insets are actually dispatched (edge-to-edge)
If you’re using edge-to-edge, ensure you’ve disabled decor fitting (directly or via enableEdgeToEdge()), otherwise IME insets/state can be wrong. (stackoverflow.com)
Citations:
- 1: https://developer.android.com/develop/ui/compose/system/insets-ui?utm_source=openai
- 2: https://developer.android.com/develop/ui/compose/performance/bestpractices?utm_source=openai
- 3: https://stackoverflow.com/questions/68847559/how-can-i-detect-keyboard-opening-and-closing-in-jetpack-compose?utm_source=openai
- 4: https://stackoverflow.com/questions/68847559/how-can-i-detect-keyboard-opening-and-closing-in-jetpack-compose?utm_source=openai
IME inset를 컴포지션 단계에서 직접 읽으면 한 프레임 늦음
WindowInsets.ime의 값은 컴포지션 이후, 레이아웃 단계 이전에 업데이트됩니다. 따라서 컴포지션 내에서 getBottom()으로 직접 읽으면 한 프레임 늦은 값을 사용하게 되어, IME 표시/숨김 시 레이아웃 깜빡임이 발생할 수 있습니다.
remember { derivedStateOf { … } }로 감싸면 이 문제가 해결되며, 더 나은 접근은 Modifier.imePadding() 또는 Modifier.windowInsetsPadding(WindowInsets.ime)를 사용하여 레이아웃 단계에서 inset을 처리하는 것입니다.
🐛 수정 제안: 수동 inset 읽기 필요 시 derivedStateOf 사용
-val density = LocalDensity.current
-val isImeVisible = WindowInsets.ime.getBottom(density) > 0
+val density = LocalDensity.current
+val isImeVisible by remember {
+ derivedStateOf { WindowInsets.ime.getBottom(density) > 0 }
+}또는 가장 좋은 방법: Modifier.imePadding() 사용으로 패딩을 자동 처리
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt`
around lines 161 - 162, The code reads WindowInsets.ime.getBottom(density)
directly in composition (val density = LocalDensity.current; val isImeVisible =
WindowInsets.ime.getBottom(density) > 0) which can be one frame late; wrap the
read in a remembered derived state (e.g., remember { derivedStateOf {
WindowInsets.ime.getBottom(density) > 0 } }) so the IME inset is tracked
correctly, or better yet remove manual reading and apply Modifier.imePadding() /
Modifier.windowInsetsPadding(WindowInsets.ime) on the composable to let the
layout stage handle insets.
| fun NavGraphBuilder.questCommonGraph( | ||
| navigateToQuest: () -> Unit, | ||
| navigateToQuestCommonComplete: (Long) -> Unit, | ||
| navigateUp: () -> Unit, | ||
| paddingValues: PaddingValues, | ||
| ) { | ||
| routeNavigation<QuestCommonRoute, QuestCommon> { | ||
| composable<QuestCommon> { | ||
| QuestCommonRoute( | ||
| navigateToQuest = navigateToQuest, | ||
| navigateToQuestCommonComplete = navigateToQuestCommonComplete, | ||
| navigateUp = navigateUp, | ||
| paddingValues = paddingValues | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for QuestCommonComplete destination registration
rg -n --type=kt 'composable<\s*QuestCommonComplete'
# Search for all QuestCommonComplete references for context
rg -n --type=kt 'QuestCommonComplete' -C 2Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 128
🏁 Script executed:
# Search for QuestCommonComplete destination registration
rg -n 'composable<\s*QuestCommonComplete' --type-add 'kotlin:*.kt' -t kotlin
# Search for all QuestCommonComplete references for context
rg -n 'QuestCommonComplete' --type-add 'kotlin:*.kt' -t kotlin -C 2Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 8099
🏁 Script executed:
cat -n app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1926
🏁 Script executed:
# Search for routeNavigation definition
rg -n 'fun.*routeNavigation' --type-add 'kotlin:*.kt' -t kotlin -A 5
# Search for composable<QuestCommonComplete> in entire codebase
rg -n 'composable.*QuestCommonComplete' --type-add 'kotlin:*.kt' -t kotlinRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 849
🏁 Script executed:
# Check QuestCommonScreen usage of navigateToQuestCommonComplete
rg -n 'navigateToQuestCommonComplete' app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.kt -B 2 -A 2
# Check if QuestCommonComplete is registered in parent graphs
rg -n 'composable<QuestCommonComplete' app/src/main/java/com/byeboo/app/presentation/quest/navigation/ --type-add 'kotlin:*.kt' -t kotlin
# Also check MainNavHost for QuestCommonComplete registration
rg -n 'composable<QuestCommonComplete' app/src/main/java/com/byeboo/app/presentation/main/ --type-add 'kotlin:*.kt' -t kotlinRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 257
🏁 Script executed:
cat -n app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 6366
🏁 Script executed:
cat -n app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 2565
🏁 Script executed:
# Check all Quest-related navigation files for any QuestCommonComplete composable
fd -e kt . app/src/main/java/com/byeboo/app/presentation/quest --exec rg -l 'QuestCommonComplete' {} \;
# Look at the Quest parent navigation to understand structure
cat -n app/src/main/java/com/byeboo/app/presentation/quest/navigation/QuestNavigation.kt | head -130Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 5836
QuestCommonComplete 목적지 등록 누락
navigateToQuestCommonComplete 확장 함수가 존재하고 QuestCommonComplete(questId)로 이동하려 하지만, questCommonGraph에는 composable만 등록되어 있습니다. 상위 questGraph에도 composable가 없어 이 함수가 호출되면 런타임 오류가 발생합니다. questCommonGraph에 composable를 추가해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.kt`
around lines 32 - 47, The questCommonGraph currently only registers
composable<QuestCommon> but the navigation callback
navigateToQuestCommonComplete expects a destination
composable<QuestCommonComplete>; add a composable<QuestCommonComplete>
registration inside questCommonGraph that maps to the QuestCommonComplete screen
and uses the questId argument so navigateToQuestCommonComplete(questId) resolves
at runtime; ensure the routeNavigation block includes both
composable<QuestCommon> (which calls QuestCommonRoute) and a new
composable<QuestCommonComplete> entry that instantiates the corresponding
screen/component and reads the Long questId parameter.
| @Composable | ||
| fun QuestCommonRoute( | ||
| navigateToQuest: () -> Unit, | ||
| navigateToQuestCommonComplete: (Long) -> Unit, | ||
| navigateUp: () -> Unit, | ||
| paddingValues: PaddingValues, | ||
| modifier: Modifier = Modifier, | ||
| viewModel: QuestCommonViewModel = hiltViewModel() | ||
| ) { | ||
| val uiState by viewModel.uiState.collectAsStateWithLifecycle() | ||
|
|
||
| QuestCommonScreen( | ||
| uiState = uiState, | ||
| paddingValues = paddingValues, | ||
| onBackClick = viewModel::onBackClicked, | ||
| onCompleteClick = viewModel::onCompleteClicked, | ||
| onUpdateContent = viewModel::updateContent, | ||
| modifier = modifier | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for QuestCommonSideEffect and side-effect collection patterns
rg -n --type=kt -C2 'QuestCommonSideEffect|sideEffect|collect.*QuestCommon'Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 101
🏁 Script executed:
#!/bin/bash
# Search for where navigateToQuestCommonComplete and navigateUp are used
rg -n --type=kt -C2 'navigateToQuestCommonComplete|navigateUp'Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 101
🏁 Script executed:
#!/bin/bash
# Find the QuestCommonViewModel file
fd --type f -name '*QuestCommonViewModel*' -o -name '*QuestCommon*ViewModel*'Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 305
🏁 Script executed:
#!/bin/bash
# Get overall file structure for quest common feature
fd --type f -path '*/quest/common/*' -name '*.kt' | head -20Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 243
🏁 Script executed:
#!/bin/bash
# Search for QuestCommonSideEffect and side-effect collection patterns
rg 'QuestCommonSideEffect|sideEffect|collect.*QuestCommon' -C 2Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 50387
🏁 Script executed:
#!/bin/bash
# Search for where navigateToQuestCommonComplete and navigateUp are used
rg 'navigateToQuestCommonComplete|navigateUp' -C 2Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 24025
🏁 Script executed:
#!/bin/bash
# Find files in quest/common directory
find . -path '*/quest/common/*' -name '*.kt'Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 535
🏁 Script executed:
#!/bin/bash
# Find QuestCommonViewModel
find . -name '*QuestCommonViewModel*'Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 167
🏁 Script executed:
#!/bin/bash
# Read QuestCommonScreen.kt to see full implementation including LaunchedEffect
cat -n app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 6366
🏁 Script executed:
#!/bin/bash
# Read QuestCommonRoute.kt if it exists
cat -n app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonRoute.kt 2>/dev/null || echo "File not found"Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 613
🏁 Script executed:
#!/bin/bash
# Read QuestCommonViewModel.kt to see full side-effect handling
cat -n app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 2565
🏁 Script executed:
#!/bin/bash
# Read QuestCommonState.kt to see side-effect definition
cat -n app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonState.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 792
QuestCommonRoute에서 네비게이션 콜백이 사용되지 않음 - 수정 필수
검증 결과, 지적된 문제가 확인되었습니다:
- QuestCommonRoute에 side-effect 수집 로직 부재: LaunchedEffect가 없어 ViewModel의 sideEffect를 구독하지 않음
- ViewModel의 네비게이션 핸들러가 비어있음:
onBackClicked()(line 24-26)와onCompleteClicked()(line 28-30)이 아무 구현이 없음 - QuestCommonSideEffect 정의 불완전:
NavigateToQuest만 정의되어 있고NavigateToQuestCommonComplete/NavigateUp이벤트 누락 - 전달받은 콜백이 사용되지 않음:
navigateToQuestCommonComplete,navigateUp콜백이 Route에서 선언되지만 전혀 호출되지 않음
완료 및 뒤로 가기 네비게이션이 동작하지 않습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.kt`
around lines 38 - 56, QuestCommonRoute currently doesn't observe ViewModel side
effects and never calls the passed navigation lambdas; add a LaunchedEffect in
QuestCommonRoute to collect viewModel.sideEffect (e.g., collectLatest) and map
emitted QuestCommonSideEffect cases to call navigateToQuest,
navigateToQuestCommonComplete(id) and navigateUp accordingly; also update the
ViewModel methods onBackClicked() and onCompleteClicked() to emit the new side
effects (NavigateUp and NavigateToQuestCommonComplete with the quest id) and
extend the QuestCommonSideEffect sealed class to include
NavigateToQuestCommonComplete(Long) and NavigateUp so the Route's collector can
dispatch the passed callbacks.
| fun onBackClicked() { | ||
|
|
||
| } | ||
|
|
||
| fun onCompleteClicked() { | ||
|
|
||
| } |
There was a problem hiding this comment.
뒤로가기/완료 핸들러가 비어 있어 UI 이벤트가 무시됩니다
현재 onBackClicked/onCompleteClicked가 no-op이라 클릭 시 아무 동작도 하지 않습니다. 최소한 sideEffect emit 또는 불필요하다면 UI에서 직접 네비게이션을 처리하도록 정리해 주세요.
🛠️ 예시(네비게이션 sideEffect emit)
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
...
fun onBackClicked() {
-
+ viewModelScope.launch {
+ _sideEffect.emit(QuestCommonSideEffect.NavigateToQuest)
+ }
}🧰 Tools
🪛 detekt (1.23.8)
[warning] 24-26: This empty block of code can be removed.
(detekt.empty-blocks.EmptyFunctionBlock)
[warning] 28-30: This empty block of code can be removed.
(detekt.empty-blocks.EmptyFunctionBlock)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.kt`
around lines 24 - 30, QuestCommonViewModel currently has no-op handlers
(onBackClicked/onCompleteClicked) so UI clicks are ignored; implement these to
either emit navigation sideEffects or remove the handlers and let the UI handle
navigation. Inside QuestCommonViewModel, update onBackClicked to emit a
sideEffect event (e.g., NavigateBack) via your existing side-effect channel (the
same mechanism used elsewhere in this VM), and update onCompleteClicked to emit
a Complete/Finish sideEffect (or a ConfirmComplete event) that the UI observes;
if you prefer not to emit from the ViewModel, delete these empty methods and
wire the UI to perform navigation directly instead. Ensure you reference the
existing side-effect flow/channel used in this class to keep behavior consistent
and test that the Fragment/Activity reacts to the new events.
| QuestTextField( | ||
| value = uiState.questAnswer, | ||
| onValueChange = { | ||
| if (it.length <= 500) { | ||
| onUpdateContent(isFocused.value, it) | ||
| } | ||
| }, | ||
| placeholder = "글로 적다 보면, 스스로에게 한 걸음 더 가까워질 수 있어요.", | ||
| onFocusChanged = { | ||
| isFocused.value = it | ||
| }, | ||
| scrollState = scrollState, | ||
| ) |
There was a problem hiding this comment.
500자 초과 시 전체 입력이 무시됨 — 붙여넣기 UX 문제
현재 it.length <= 500 조건이 false이면 onValueChange 콜백 자체가 호출되지 않아, 500자를 초과하는 텍스트를 붙여넣을 경우 아무 동작도 일어나지 않습니다. 사용자는 붙여넣기가 적용된 것처럼 보이다 원상 복구되는 현상을 경험하며, 별도의 피드백도 없습니다.
🛠️ 500자로 잘라내는 방식으로 개선
onValueChange = {
- if (it.length <= 500) {
- onUpdateContent(isFocused.value, it)
- }
+ onUpdateContent(isFocused.value, it.take(500))
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt`
around lines 221 - 233, The onValueChange currently ignores any input when
it.length > 500, causing pasted text >500 chars to be dropped; change the
handler inside QuestTextField so it always calls
onUpdateContent(isFocused.value, newValueTrimmed) but trims the incoming text to
500 characters (e.g., val v = if (it.length <= 500) it else it.take(500)) and
pass v to onUpdateContent; update references in the QuestTextField onValueChange
block and ensure uiState.questAnswer continues to reflect the trimmed value so
the UI and clipboard UX match.
| onClick = { onCommonQuestClick(0) }, | ||
| buttonText = "답변 작성하기", |
There was a problem hiding this comment.
questId 하드코딩(0)으로 잘못된 화면 이동 위험
현재 항상 0을 전달해 실제 퀘스트 식별자가 반영되지 않습니다. 상태에서 실제 questId를 전달하도록 연결해 주세요(해당 필드가 없다면 플로우에 추가 전달 필요).
🛠️ 예시 수정
- onClick = { onCommonQuestClick(0) },
+ onClick = { onCommonQuestClick(state.questId) },📝 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.
| onClick = { onCommonQuestClick(0) }, | |
| buttonText = "답변 작성하기", | |
| onClick = { onCommonQuestClick(state.questId) }, | |
| buttonText = "답변 작성하기", |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/screen/CommonJourneyScreen.kt`
around lines 145 - 146, The click handler is hardcoding questId as 0
(onCommonQuestClick(0)), causing wrong navigation; change it to pass the real
quest identifier from the UI state/props (e.g., use the Quest item's id or the
screen state variable) so the correct quest is opened. Locate the onClick
invocation that calls onCommonQuestClick(0) in CommonJourneyScreen and replace
the literal 0 with the actual questId source (for example item.id, quest.id, or
a questId property supplied to the composable); if that property isn't
available, thread the questId through the composable parameters or flow and
update the caller to provide it. Ensure onCommonQuestClick is still used for
navigation but receives the real identifier.
fredleeJH
left a comment
There was a problem hiding this comment.
고생 많으셨습니다! 확인 한 번 부탁드립니다~~
| Modifier | ||
| .fillMaxWidth() | ||
| .padding(horizontal = screenWidthDp(48.dp)), | ||
| dialogProperties = DialogProperties(usePlatformDefaultWidth = false), |
There was a problem hiding this comment.
이 부분 이전 PR에서 소희님이 QuestQuitModal 정의 시 dialogProperties를 디폴트 인자로 넣어두었습니다. 별도로 설정을 변경하실 게 아니라면 호출부에서는 생략 하셔도 될거 같습니다! 머지하실 때 참고 부탁드립니다~
There was a problem hiding this comment.
저 오늘 모달 수정해놓은 피알 머지해놔갖구 그거 pull 당겨서 해주시면 될 것 같아요!!
화이팅~!!
| isPhotoQuestion = true, | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(bottom = 14.dp) |
There was a problem hiding this comment.
| .padding(bottom = 14.dp) | |
| .padding(bottom = screenHeightDp(14.dp)) |
| Text( | ||
| text = "(${imageCount}/1)", | ||
| color = ByeBooTheme.colors.gray400, | ||
| style = ByeBooTheme.typography.body5, |
| } | ||
| }, | ||
| placeholder = "꼭 적지 않아도 괜찮지만, 글로 정리해 보면 스스로에게 한 걸음 더 가까워질 수 있어요.", | ||
| onFocusChanged = { |
There was a problem hiding this comment.
그냥 이렇게 축약해도 되지 않을까요?
| onFocusChanged = { | |
| onFocusChanged = onFocusChanged |
| import com.byeboo.app.domain.model.quest.QuestWritingState | ||
|
|
||
| @Immutable | ||
| data class QuestCommonState( |
There was a problem hiding this comment.
버튼 활성화 값을 뷰모델에서 매번 계산해서 넣어주는 대신, 데이터 클래스 안에서 자동으로 계산되게 하면 어떨까요?
| data class QuestCommonState( | |
| data class QuestCommonState( | |
| val question: String = "", | |
| val questAnswer: String = "", | |
| val originalAnswer: String = "", | |
| val isEditMode: Boolean = false, | |
| val contentsState: QuestWritingState = QuestWritingState.Empty, | |
| ) { | |
| val hasAnswerChanged: Boolean | |
| get() = questAnswer != originalAnswer | |
| val isCompleteButtonEnabled: Boolean get() { | |
| val isValid = QuestContentLengthValidator.validButton(questAnswer) | |
| return if (isEditMode) isValid && hasAnswerChanged else isValid | |
| } | |
| } |
|
|
||
| } | ||
|
|
||
| fun updateContent( |
There was a problem hiding this comment.
위에서 제안 드린 대로 state를 작성하면 이런식으로 쓸 수 있을 것 같습니다!
| fun updateContent( | |
| fun updateContent(isFocused: Boolean, questAnswer: String) { | |
| val contentState = QuestContentLengthValidator.validate(isFocused, questAnswer) | |
| _uiState.update { prev -> | |
| prev.copy( | |
| questAnswer = questAnswer, | |
| contentsState = contentState | |
| ) | |
| } | |
| } |
| } | ||
| } | ||
|
|
||
| private fun completeButtonEnabled(state: QuestCommonState): Boolean { |
There was a problem hiding this comment.
그렇게 하면 이 부분은 그냥 지워도 될 것 같습니다!
|
|
||
| append(text = maxCharCount.toString()) | ||
| }, | ||
| style = ByeBooTheme.typography.body6, |
|
|
||
| Spacer(modifier = Modifier.height(screenHeightDp(12.dp))) | ||
|
|
||
| Text( |
There was a problem hiding this comment.
gray50, head2 확인 부탁드립니다!
| @@ -1,4 +1,4 @@ | |||
| package com.byeboo.app.presentation.quest.behavior | |||
| package com.byeboo.app.presentation.quest.behavior.complete | |||
There was a problem hiding this comment.
이렇게 패키지 분리하니 넘 깔끔하네요!!!
| modifier = | ||
| modifier | ||
| .width(screenWidthDp(96.dp)) | ||
| .width(screenWidthDp(312.dp)) |
There was a problem hiding this comment.
p1: 이거 기기대응 해주려면 가로 너비를 고정값을 주면 안 될 것 같아요..!!
fillMaxWidth로 주고 가로 패딩 나중에 상위에서 주고 aspectRatio 이거 활용하면 되지 않을까 싶은데 수정 부탁드립니다!!
| Modifier | ||
| .fillMaxWidth() | ||
| .padding(horizontal = screenWidthDp(48.dp)), | ||
| dialogProperties = DialogProperties(usePlatformDefaultWidth = false), |
There was a problem hiding this comment.
저 오늘 모달 수정해놓은 피알 머지해놔갖구 그거 pull 당겨서 해주시면 될 것 같아요!!
화이팅~!!
| sealed class QuestCommonRoute : Route { | ||
|
|
||
| @Serializable | ||
| data class QuestCommon( |
There was a problem hiding this comment.
p3: 이거 다른 화면들처럼 작성 화면은 ~ writing으로 하는거 어떠세요??
그래서 이건 QuestCommonWriting으로 하고 관련 화면들 이름도 이런식으로요!!
| modifier = | ||
| modifier | ||
| .fillMaxWidth() | ||
| .padding(horizontal = 24.dp), |
There was a problem hiding this comment.
p1: 여기 지금 가로 패딩이 상위에서 중복으로 들어가고 있습니다!!
상위 Column에서 한 번에 24 가로 패딩을 주고 있으니 여기 빼도 될 것 같아요!
| }, | ||
| style = ByeBooTheme.typography.body6, | ||
| color = ByeBooTheme.colors.gray400, | ||
| textAlign = TextAlign.End, |
There was a problem hiding this comment.
p3: Text가 wrapContent 상태라 textAlign = TextAlign.End는 실제로는 동작하지 않고 있습니다!
현재 Spacer(weight = 1f)로 이미 우측 정렬이 되어 있어서 제거해도 동일하게 동작해 없애도 될 것 같아요!!
| import com.byeboo.app.core.util.screenWidthDp | ||
|
|
||
| @Composable | ||
| fun QuestWritingTopBar( |
There was a problem hiding this comment.
P3: 개인적인 건의사항..ㅎㅎ
모달처럼 이거 탑바 저희 basicTopBar 하나 만들고 QuestWritingTopBar 이것처럼 만들어서 쓰는거 어떠세요???
작성화면만 공통 탑바 만들지 말고 작성완료 화면도 탑바 동일하니 공통으로 만들어서 쓰는 방향으로요!!
그렇게 하면서 패키지도 component/topbar 안에 위치시키면 좋을 것 같아요!!
There was a problem hiding this comment.
아님 요거 전체적으로 다 건드려야 되니까 바쁘시면 제가 할게여!!
There was a problem hiding this comment.
갓 소 희
이것도 혹시 미뤄봐도 될까요?^___^
| onCompleteClick: () -> Unit, | ||
| ) { | ||
| Row( | ||
| modifier = Modifier.padding(horizontal = screenWidthDp(22.dp)), |
There was a problem hiding this comment.
p2: 공통 컴포넌트이니 modifier 뚫어놓는거 어떠세요?/
app/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.kt
Show resolved
Hide resolved
| Spacer(modifier = Modifier.height(screenHeightDp(16.dp))) | ||
| ByeBooButton( | ||
| onClick = { /* TODO : 답변 작성 화면 이동 */ }, | ||
| onClick = { onCommonQuestClick(0) }, |
There was a problem hiding this comment.
p1: 이거 questId 자체로 넣어놔야 되는거 아닌가여..????
There was a problem hiding this comment.
ㅋㅋㅋㅋ 이거 네비만 확인해보려고 그냥 임시로 넣어놨어요.... 리드님이 해결해주시면 정말 좋겟ㅆ다..제발...한번만₩~~
sohee6989
left a comment
There was a problem hiding this comment.
까다로운 작업 수고하셨습니다!!
리뷰 확인 부탁드립니다아~~
| Dialog( | ||
| onDismissRequest = {}, | ||
| properties = | ||
| DialogProperties( | ||
| usePlatformDefaultWidth = false, | ||
| dismissOnBackPress = false, | ||
| dismissOnClickOutside = false, | ||
| ), | ||
| ) { | ||
| QuestCompleteCard( | ||
| modifier = | ||
| Modifier | ||
| .fillMaxWidth() | ||
| .padding(horizontal = screenWidthDp(24.dp)), | ||
| ) | ||
| } |
There was a problem hiding this comment.
p2: QuestCompleteCard에 이 Dialog까지 넣어줘도 될 것 같아요!!
| Spacer(modifier = Modifier.height(screenHeightDp(16.dp))) | ||
| Spacer(modifier = Modifier.height(screenHeightDp(24.dp))) | ||
|
|
||
| LazyColumn( |
There was a problem hiding this comment.
p1: 지금 item 하나에 Column을 사용해서 화면 전체를 그리고 있는데 그러면 LazyColumn을 쓰는 의미가 없는 것 같습니다..!!
지금 상태면 그냥 Column에 verticalscroll 주는게 더 나은것 같다고 생각이 드는데 의견이 궁금합니다!
There was a problem hiding this comment.
소희님 의견에 동의합니다!
이번 스프린트부터 UI를 컴포넌트화 해주어서 기존에 쓰던 LazyColumn 보다는 verticalscroll 이 낫다고 생각해요!
수정하겠습니다!!
| buttonTextColor = ByeBooTheme.colors.white, | ||
| buttonStyle = ByeBooTheme.typography.body2, | ||
| buttonBackgroundColor = ByeBooTheme.colors.primary300, | ||
| onClick = {}, |
There was a problem hiding this comment.
p2: 요거 onClick 빈 람다로 두는 것 대신 TODO로 남겨놓든가 상위 호이스팅 부탁드릴게요!!
There was a problem hiding this comment.
p1: 이 화면에는 보리에게 답장 받기 버튼 안 해놓으셨어요!
| questNumber: Long? = null, | ||
| question: String, | ||
| onTipClick: (() -> Unit)? = null, | ||
| modifier: Modifier = Modifier, |
| style = ByeBooTheme.typography.body2, | ||
| ) | ||
|
|
||
| Spacer(modifier = modifier.width(screenWidthDp(8.dp))) |
There was a problem hiding this comment.
p2: 이거 대신에 Row에서 horizontalArrangement = Arrangement.spacedBy(screenWidthDp(8.dp)) 어떠세요??
| ) | ||
| } | ||
|
|
||
| Spacer(modifier = modifier.height(screenHeightDp(12.dp))) |
There was a problem hiding this comment.
p2: 이것도 Column에서 주는거 어떠세요??
| textStyle = ByeBooTheme.typography.cap1, | ||
| ) | ||
|
|
||
| Spacer(modifier = modifier.width(screenWidthDp(8.dp))) |
| ) | ||
| } | ||
|
|
||
| Spacer(modifier = modifier.height(screenHeightDp(8.dp))) |
There was a problem hiding this comment.
p2: Column에서 주는거 어떠세요??
| keyboardActions = | ||
| KeyboardActions(onDone = { | ||
| keyboardController?.hide() | ||
| focusManager.clearFocus() | ||
| }), |
There was a problem hiding this comment.
p2: keyboardActions 이거 넣어주는거 어떠세요?
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (6)
app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt (1)
77-86:⚠️ Potential issue | 🟡 Minor
decorationBox에Box래퍼 필요 — placeholder가 수직으로 쌓임
placeholder와innerTextField()가Box없이 순차적으로 호출되어 플레이스홀더가 텍스트 입력 영역 위에 오버레이되지 않고 수직으로 배치됩니다.🐛 제안된 수정
decorationBox = { innerTextField -> + Box(modifier = Modifier.fillMaxWidth()) { if (value.isEmpty() && !(isFocused.value)) { Text( text = placeholder, color = ByeBooTheme.colors.gray300, style = ByeBooTheme.typography.body3, ) } innerTextField() + } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt` around lines 77 - 86, The decorationBox implementation in QuestTextField (the lambda assigned to decorationBox) renders Text(placeholder) and innerTextField() sequentially so the placeholder stacks vertically instead of overlaying; wrap the placeholder and innerTextField() in a Box so the placeholder is layered behind/above the inner text field, using Box { if (value.isEmpty() && !isFocused.value) Text(...) ; innerTextField() } and keep the same styling and focus checks (references: decorationBox, innerTextField, value, placeholder, isFocused).app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt (1)
213-217:⚠️ Potential issue | 🟡 Minor500자 초과 붙여넣기 시 전체 입력이 무시됨
현재 구현에서 500자를 초과하는 텍스트를 붙여넣으면 아무 동작도 일어나지 않습니다. 사용자에게 별도의 피드백 없이 입력이 무시되어 UX 문제가 발생할 수 있습니다.
🛠️ 500자로 자르는 방식으로 개선
onValueChange = { - if (it.length <= 500) { - onUpdateContent(isFocused.value, it) - } + onUpdateContent(isFocused.value, it.take(500)) },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt` around lines 213 - 217, 현재 onValueChange 블록은 길이 500자를 초과하는 붙여넣기를 완전히 무시하므로, 입력이 500자를 초과하면 잘라서 처리하도록 변경하세요: in the onValueChange lambda check if it.length > 500 then create a truncated variable (e.g. val truncated = it.take(500)) and call onUpdateContent(isFocused.value, truncated) so only the first 500 chars are accepted; keep the existing behavior for it.length <= 500. Ensure you update the same lambda where onValueChange = { ... } is defined so the UI reflects the truncated input.app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.kt (1)
25-47:⚠️ Potential issue | 🔴 Critical완료 목적지 미등록으로
navigateToQuestCommonComplete호출 시 런타임 실패 위험이 있습니다.완료 이동 API는 추가됐지만 그래프에는 작성 화면만 등록되어 있어 완료 라우트가 해석되지 않습니다.
questCommonGraph에 완료 목적지도 함께 등록해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.kt` around lines 25 - 47, questCommonGraph에 완료 목적지인 QuestCommonComplete가 등록되어 있지 않아 navigateToQuestCommonComplete 호출 시 경로 해석 실패가 발생합니다; routeNavigation 블록 안에 QuestCommonComplete 목적지를 composable로 추가하여 QuestCommonComplete 라우트를 등록하고, NavGraphBuilder.questCommonGraph의 파라미터 navigateToQuestCommonComplete(Long)와 NavController.navigateToQuestCommonComplete(questId) 호출이 해당 목적지(예: QuestCommonComplete)로 이동하도록 구성하세요; 등록 시 기존 QuestCommonWriting composable과 동일한 네이밍/타입(QuestCommonComplete)을 사용하고 필요하면 해당 목적지용 화면 컴포저블을 호출하도록 연결합니다.app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.kt (1)
24-30:⚠️ Potential issue | 🟠 Major뒤로가기/완료 핸들러가 비어 있어 상단 버튼이 동작하지 않습니다.
현재 두 메서드가 no-op이라 클릭 이벤트가 전부 무시됩니다.
sideEffectemit으로 연결하거나, ViewModel 경유가 불필요하면 Route에서 네비게이션 콜백을 직접 연결해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.kt` around lines 24 - 30, The onBackClicked and onCompleteClicked methods in QuestCommonViewModel are no-ops so top-bar buttons do nothing; update these methods to emit appropriate side effects (e.g., sideEffect.emit(SideEffect.NavigateBack) and sideEffect.emit(SideEffect.Complete) or similarly named events used in this ViewModel) or, if navigation is handled outside the ViewModel, invoke the provided navigation callbacks from Route instead of leaving onBackClicked/onCompleteClicked empty; ensure you reference and use the ViewModel's sideEffect flow and the exact method names onBackClicked and onCompleteClicked when wiring the actions so the UI receives the events.app/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteScreen.kt (1)
144-150:⚠️ Potential issue | 🟠 Major완료 액션 버튼이 현재 무동작입니다.
onClick이 주석만 있는 no-op이라 탭해도 반응이 없습니다. 실제 동작 콜백을 호이스팅해 연결하거나, 연결 전까지는 버튼을 비노출/비활성 처리해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteScreen.kt` around lines 144 - 150, The completion action button currently has a no-op onClick in ByeBooButton inside QuestRecordingCompleteScreen; either wire a real callback from the parent (hoist an onReplyClicked parameter into QuestRecordingCompleteScreen and pass it down to ByeBooButton's onClick) or make the button non-interactive until connected (set the button to disabled/hidden by toggling an isEnabled or isVisible flag and render accordingly). Update the function signature that contains ByeBooButton to accept the callback (e.g., onReplyClicked: ()->Unit) or an isEnabled boolean and use those to drive ByeBooButton's onClick and visual state so the button is never silently non-responsive.app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt (1)
210-216:⚠️ Potential issue | 🟠 Major핵심 CTA가 아직 미구현입니다.
Line 215onClick이 TODO로 비어 있어"보리에게 답장 받기"버튼이 동작하지 않습니다.원하시면
QuestBehaviorCompleteRoute에 콜백을 추가해서 실제 네비게이션까지 연결하는 최소 패치를 작성해드릴게요. 이 항목을 별도 이슈로 열까요?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt` around lines 210 - 216, The primary CTA button "보리에게 답장 받기" uses ByeBooButton with an empty onClick TODO so it doesn't perform navigation; implement the click handler by wiring it to the screen's navigation callback (add a parameter/callback to QuestBehaviorCompleteRoute if not present) and invoke that callback from the ByeBooButton's onClick to perform the intended navigation/action; locate the ByeBooButton instance in QuestBehaviorCompleteScreen/QuestBehaviorCompleteRoute and replace the TODO with a call to the provided callback (or create a new lambda parameter on QuestBehaviorCompleteRoute such as onReplyClicked and forward it to the screen) so the button triggers the actual navigation.
🧹 Nitpick comments (4)
app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt (1)
47-48: 주석으로 남은 이전 레이아웃 코드는 정리하는 편이 좋습니다.동작하지 않는 과거 modifier가 남아 있으면 이후 수정 시 혼선을 줍니다.
정리 예시 diff
modifier - //.width(screenWidthDp(312.dp)) .fillMaxWidth() .aspectRatio(1f)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt` around lines 47 - 48, 주석으로 남아있는 이전 레이아웃 코드(.width(screenWidthDp(312.dp)))를 정리해 혼선을 없애세요: QuestPhotoPicker 내 해당 Modifier 체인(현재 .fillMaxWidth()가 적용된 곳)을 찾아 주석 처리된 .width(...) 라인을 삭제하여 불필요한 과거 코드가 남지 않도록 정리합니다.app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt (1)
37-37: 사용되지 않는 변수 제거 필요
lastLineBottom이 선언되었지만 사용되지 않습니다.onTextLayout에서 값을 설정하지만 어디서도 읽지 않습니다.♻️ 제안된 수정
val isFocused = remember { mutableStateOf(false) } - val lastLineBottom = remember { mutableStateOf(0) } val keyboardController = LocalSoftwareKeyboardController.current그리고
onTextLayout콜백도 제거:- onTextLayout = { layoutResult -> - lastLineBottom.value = - layoutResult.getLineBottom(layoutResult.lineCount - 1).toInt() - },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt` at line 37, Remove the unused mutable state and associated callback: delete the declaration of lastLineBottom (the remember { mutableStateOf(0) } in QuestTextField) and remove the onTextLayout callback that only sets this value, since the state is never read; ensure no other code references lastLineBottom or the removed onTextLayout to avoid dangling references.app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingViewModel.kt (1)
198-198: 결과가 사용되지 않는 Repository 호출
questRecordedDetailRepository.getQuestRecordedDetail(questId)호출 결과가 사용되지 않습니다. 캐시 갱신 목적이라면 의도를 명확히 하는 주석을 추가하거나, 불필요하다면 제거를 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingViewModel.kt` at line 198, questRecordedDetailRepository.getQuestRecordedDetail(questId) 호출 결과가 사용되지 않으니 의도를 명확히 하세요: 만약 캐시 갱신 목적이면 QuestRecordingViewModel 내 해당 호출을 명시적으로 비동기 실행(예: viewModelScope.launch)하고 예외를 처리하는 주석과 함께 호출 목적을 주석으로 남기거나 반환값/Flow를 변수(예: val _ = ...)에 할당해 의도를 표현하세요; 불필요한 호출이면 해당 라인을 제거하세요.app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt (1)
117-120: LaunchedEffect 재시작 가능성
LaunchedEffect(Unit)은 recomposition 시 재시작되지 않지만,showCompleteModal이false에서true로 다시 변경되면 새로운LaunchedEffect가 시작됩니다. 모달이 여러 번 표시될 경우 예상치 못한 동작이 발생할 수 있으므로, ViewModel에서 타임아웃을 처리하거나 고유 키를 사용하는 것을 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt` around lines 117 - 120, The LaunchedEffect(Unit) block starting in QuestRecordingScreen can restart when showCompleteModal toggles and cause multiple timers; move the timeout logic into the ViewModel or tie the LaunchedEffect to a unique, modal-specific key so it only runs once per modal show. Specifically, remove or replace the LaunchedEffect(Unit) that calls viewModel.onCompleteModalTimeout(), and implement the timeout in the ViewModel (e.g., start a single coroutine/job when showCompleteModal is set true and cancel when false) or change the LaunchedEffect key to something like showCompleteModalId so viewModel.onCompleteModalTimeout() is invoked only once per modal instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.kt`:
- Around line 26-27: QuestRecordedDetailResponseDto currently defines
aiAnswerExists as a non-null Boolean which will throw MissingFieldException if
the server omits the field; update the DTO to provide a safe default (e.g.,
aiAnswerExists = false) or make it nullable (Boolean?) so deserialization won't
crash, and apply the same defensive pattern to other DTOs with non-null Booleans
(e.g., KakaoLoginResponseDto.isRegistered, NotificationResponseDto.alarmEnabled)
to keep behavior consistent.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt`:
- Around line 207-209: The Spacer inside LazyColumn's item is using
Modifier.weight(1f) which causes a scope mismatch (weight is only available in
ColumnScope/RowScope); wrap the item content in a Column (e.g., Column(modifier
= Modifier.fillMaxHeight())) or replace the item with a container (Box/Column)
that provides ColumnScope, then move Spacer(Modifier.weight(1f)) inside that
Column so it composes correctly; also replace the placeholder /*Todo: ai 버튼 연결
*/ in the Button onClick with the real handler (call an existing ViewModel or
navigation method like viewModel.onAiButtonClicked() or a passed lambda) so the
AI button is wired up.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt`:
- Around line 89-93: The Icon currently sets contentDescription = null which
makes the clickable upload control inaccessible; change it to use the string
resource (R.string.quest_photo_upload) via stringResource and pass that to
contentDescription (update imports to include
androidx.compose.ui.res.stringResource), and ensure the new string resource
R.string.quest_photo_upload is added to resources; refer to the Icon usage
(ImageVector.vectorResource(id = R.drawable.ic_plus)) in QuestPhotoPicker.kt to
locate and update the contentDescription.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt`:
- Around line 139-142: In QuestBehaviorViewModel, the image input stream opened
with context.contentResolver.openInputStream(imageUrl) must be closed to avoid
resource leaks and the MIME type must be null-checked instead of calling
toString(); change the code that reads inputStream, imageBytes, contentType and
imageKey to wrap the InputStream in a use { } block (e.g., inputStream?.use {
... } ) so it is auto-closed and handle a null inputStream by throwing or
returning an error, and replace
context.contentResolver.getType(imageUrl).toString() with a null-safe check
(e.g., val contentType = context.contentResolver.getType(imageUrl) ?: error("이미지
MIME 타입을 결정할 수 없습니다.")) — apply the same pattern to the other occurrence around
lines 210-213.
- Around line 336-347: The current null check uses
requireNotNull(state.imageKey) which allows empty strings; update the validation
of imageKey in the block that builds QuestBehaviorEditModel (the variable
imageKey used before calling questBehaviorRepository.updateQuestBehavior) to
reject blank/empty values as well (e.g., require that state.imageKey is neither
null nor blank) so an empty imageKeyArg.orEmpty() cannot slip through; adjust
the require call or add an additional check on imageKey.isNotBlank() and throw a
clear error if invalid.
- Around line 128-176: onSaveClicked can leave _uiState.isUploading stuck true
because the early return (when state.selectedImageUri is null) and the
runCatching success/failure paths don't always reset it; fix by ensuring
_uiState.update { it.copy(isUploading = false) } runs on every exit: set
isUploading = true at start of onSaveClicked, then use a try { ... } finally {
_uiState.update { it.copy(isUploading = false) } } around the image
upload/runCatching logic (or explicitly update in the early-return path and both
onSuccess/onFailure handlers) so that the UI always clears the loading state
(refer to onSaveClicked, _uiState, runCatching/uploadImageUseCase).
---
Duplicate comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt`:
- Around line 210-216: The primary CTA button "보리에게 답장 받기" uses ByeBooButton
with an empty onClick TODO so it doesn't perform navigation; implement the click
handler by wiring it to the screen's navigation callback (add a
parameter/callback to QuestBehaviorCompleteRoute if not present) and invoke that
callback from the ByeBooButton's onClick to perform the intended
navigation/action; locate the ByeBooButton instance in
QuestBehaviorCompleteScreen/QuestBehaviorCompleteRoute and replace the TODO with
a call to the provided callback (or create a new lambda parameter on
QuestBehaviorCompleteRoute such as onReplyClicked and forward it to the screen)
so the button triggers the actual navigation.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.kt`:
- Around line 25-47: questCommonGraph에 완료 목적지인 QuestCommonComplete가 등록되어 있지 않아
navigateToQuestCommonComplete 호출 시 경로 해석 실패가 발생합니다; routeNavigation 블록 안에
QuestCommonComplete 목적지를 composable로 추가하여 QuestCommonComplete 라우트를 등록하고,
NavGraphBuilder.questCommonGraph의 파라미터 navigateToQuestCommonComplete(Long)와
NavController.navigateToQuestCommonComplete(questId) 호출이 해당 목적지(예:
QuestCommonComplete)로 이동하도록 구성하세요; 등록 시 기존 QuestCommonWriting composable과 동일한
네이밍/타입(QuestCommonComplete)을 사용하고 필요하면 해당 목적지용 화면 컴포저블을 호출하도록 연결합니다.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.kt`:
- Around line 24-30: The onBackClicked and onCompleteClicked methods in
QuestCommonViewModel are no-ops so top-bar buttons do nothing; update these
methods to emit appropriate side effects (e.g.,
sideEffect.emit(SideEffect.NavigateBack) and
sideEffect.emit(SideEffect.Complete) or similarly named events used in this
ViewModel) or, if navigation is handled outside the ViewModel, invoke the
provided navigation callbacks from Route instead of leaving
onBackClicked/onCompleteClicked empty; ensure you reference and use the
ViewModel's sideEffect flow and the exact method names onBackClicked and
onCompleteClicked when wiring the actions so the UI receives the events.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt`:
- Around line 77-86: The decorationBox implementation in QuestTextField (the
lambda assigned to decorationBox) renders Text(placeholder) and innerTextField()
sequentially so the placeholder stacks vertically instead of overlaying; wrap
the placeholder and innerTextField() in a Box so the placeholder is layered
behind/above the inner text field, using Box { if (value.isEmpty() &&
!isFocused.value) Text(...) ; innerTextField() } and keep the same styling and
focus checks (references: decorationBox, innerTextField, value, placeholder,
isFocused).
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteScreen.kt`:
- Around line 144-150: The completion action button currently has a no-op
onClick in ByeBooButton inside QuestRecordingCompleteScreen; either wire a real
callback from the parent (hoist an onReplyClicked parameter into
QuestRecordingCompleteScreen and pass it down to ByeBooButton's onClick) or make
the button non-interactive until connected (set the button to disabled/hidden by
toggling an isEnabled or isVisible flag and render accordingly). Update the
function signature that contains ByeBooButton to accept the callback (e.g.,
onReplyClicked: ()->Unit) or an isEnabled boolean and use those to drive
ByeBooButton's onClick and visual state so the button is never silently
non-responsive.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt`:
- Around line 213-217: 현재 onValueChange 블록은 길이 500자를 초과하는 붙여넣기를 완전히 무시하므로, 입력이
500자를 초과하면 잘라서 처리하도록 변경하세요: in the onValueChange lambda check if it.length > 500
then create a truncated variable (e.g. val truncated = it.take(500)) and call
onUpdateContent(isFocused.value, truncated) so only the first 500 chars are
accepted; keep the existing behavior for it.length <= 500. Ensure you update the
same lambda where onValueChange = { ... } is defined so the UI reflects the
truncated input.
---
Nitpick comments:
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt`:
- Around line 47-48: 주석으로 남아있는 이전 레이아웃 코드(.width(screenWidthDp(312.dp)))를 정리해
혼선을 없애세요: QuestPhotoPicker 내 해당 Modifier 체인(현재 .fillMaxWidth()가 적용된 곳)을 찾아 주석
처리된 .width(...) 라인을 삭제하여 불필요한 과거 코드가 남지 않도록 정리합니다.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt`:
- Line 37: Remove the unused mutable state and associated callback: delete the
declaration of lastLineBottom (the remember { mutableStateOf(0) } in
QuestTextField) and remove the onTextLayout callback that only sets this value,
since the state is never read; ensure no other code references lastLineBottom or
the removed onTextLayout to avoid dangling references.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt`:
- Around line 117-120: The LaunchedEffect(Unit) block starting in
QuestRecordingScreen can restart when showCompleteModal toggles and cause
multiple timers; move the timeout logic into the ViewModel or tie the
LaunchedEffect to a unique, modal-specific key so it only runs once per modal
show. Specifically, remove or replace the LaunchedEffect(Unit) that calls
viewModel.onCompleteModalTimeout(), and implement the timeout in the ViewModel
(e.g., start a single coroutine/job when showCompleteModal is set true and
cancel when false) or change the LaunchedEffect key to something like
showCompleteModalId so viewModel.onCompleteModalTimeout() is invoked only once
per modal instance.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingViewModel.kt`:
- Line 198: questRecordedDetailRepository.getQuestRecordedDetail(questId) 호출 결과가
사용되지 않으니 의도를 명확히 하세요: 만약 캐시 갱신 목적이면 QuestRecordingViewModel 내 해당 호출을 명시적으로 비동기
실행(예: viewModelScope.launch)하고 예외를 처리하는 주석과 함께 호출 목적을 주석으로 남기거나 반환값/Flow를 변수(예:
val _ = ...)에 할당해 의도를 표현하세요; 불필요한 호출이면 해당 라인을 제거하세요.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.ktapp/src/main/java/com/byeboo/app/data/mapper/quest/QuestRecordedDetailMapper.ktapp/src/main/java/com/byeboo/app/domain/model/quest/QuestRecordedDetailModel.ktapp/src/main/java/com/byeboo/app/presentation/main/MainNavHost.ktapp/src/main/java/com/byeboo/app/presentation/main/MainNavigator.ktapp/src/main/java/com/byeboo/app/presentation/quest/QuestScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorState.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonRoute.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonState.ktapp/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/card/QuestCompleteDialog.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingTitle.ktapp/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.ktapp/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/record/navigation/QuestRecordNavigation.ktapp/src/main/java/com/byeboo/app/presentation/quest/record/navigation/QuestRecordRoute.ktapp/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.ktapp/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingState.ktapp/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (5)
- app/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.kt
- app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt
- app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt
- app/src/main/java/com/byeboo/app/presentation/quest/record/navigation/QuestRecordNavigation.kt
- app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt
| @SerialName("aiAnswerExists") | ||
| val aiAnswerExists: Boolean |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# 대상 파일 확인 및 전체 구조 파악
find . -path ./app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 167
🏁 Script executed:
# 파일 내용 확인 (사이즈 먼저)
wc -l app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 168
🏁 Script executed:
# 해당 파일의 26-27 라인 및 주변 컨텍스트 확인
cat -n app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.kt | head -40Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1070
🏁 Script executed:
# aiAnswerExists 필드 관련된 모든 코드 검색
rg -n "aiAnswerExists" --type=kt -B2 -A2Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 101
🏁 Script executed:
# QuestRecordedDetailResponseDto 사용 패턴 확인
rg -n "QuestRecordedDetailResponseDto" --type=kt -B1 -A3Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 101
🏁 Script executed:
# 응답 샘플 또는 테스트 데이터 확인
rg -n '"aiAnswerExists"' --type=json --type=kt -B2 -A2Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 101
🏁 Script executed:
# 비슷한 DTO의 기본값 패턴 확인
fd "ResponseDto.kt$" -x rg -l "val.*Boolean" {} | head -5Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 427
🏁 Script executed:
# 다른 DTO 파일들의 Boolean 필드 패턴 확인
cat app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestCountResponseDto.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 439
🏁 Script executed:
# 모든 ResponseDto에서 Boolean 필드 찾기
find . -name "*ResponseDto.kt" -exec cat {} \; | grep -n "Boolean"Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 215
🏁 Script executed:
# API 응답 관련 테스트 또는 샘플 데이터 검색
find . -type f \( -name "*.json" -o -name "*test*" -o -name "*Test.kt" \) | xargs grep -l "aiAnswerExists" 2>/dev/null | head -5Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 57
🏁 Script executed:
# Retrofit 또는 API 호출 부분에서 QuestRecordedDetailResponseDto 사용 패턴
find . -name "*.kt" -exec grep -l "QuestRecordedDetailResponseDto" {} \; | head -5Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 535
🏁 Script executed:
# 응답 페이로드 및 API 문서 확인
find . -type f \( -name "*.md" -o -name "*.txt" \) | xargs grep -l "aiAnswerExists" 2>/dev/nullRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 59
🏁 Script executed:
# QuestRecordedDetailService 구현 확인
cat app/src/main/java/com/byeboo/app/data/service/quest/QuestRecordedDetailService.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 514
🏁 Script executed:
# QuestRecordedDetailDataSourceImpl 구현 확인
cat app/src/main/java/com/byeboo/app/data/datasourceimpl/remote/quest/QuestRecordedDetailDataSourceImpl.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 811
🏁 Script executed:
# 테스트 코드에서 QuestRecordedDetailResponseDto 사용 확인
find . -name "*Test*.kt" -o -name "*test*.kt" | xargs grep -l "QuestRecordedDetailResponseDto" 2>/dev/null | head -3Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 57
🏁 Script executed:
# isRegistered와 alarmEnabled 필드 정의 확인 (기본값 여부)
find . -name "*ResponseDto.kt" -exec grep -l "isRegistered\|alarmEnabled" {} \;Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 250
🏁 Script executed:
# 다른 non-null Boolean 필드들의 정의 확인
cat app/src/main/java/com/byeboo/app/data/dto/response/auth/KakaoLoginResponseDto.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 652
🏁 Script executed:
# 서버 문서나 API 스펙 확인
find . -type f \( -name "*.md" -o -name "*API*" -o -name "*api*" \) | xargs grep -l "aiAnswer\|questRecorded" 2>/dev/null | head -3Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 57
🏁 Script executed:
# 테스트 또는 mock 데이터에서 aiAnswerExists 필드 존재 여부
find . -type f -name "*.kt" | xargs grep -B5 -A5 '"aiAnswerExists"\|aiAnswerExists.*=' 2>/dev/null | head -20Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 1896
🏁 Script executed:
# 다른 non-null Boolean 필드들의 기본값 패턴 최종 확인
grep -rn "Boolean[^?]" app/src/main/java/com/byeboo/app/data/dto/response/ --include="*.kt" | head -10Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 313
필수 Boolean 필드 추가로 역직렬화 실패 가능성이 있습니다.
Line 26~27의 aiAnswerExists는 기본값 없는 non-null 필드로, 서버 응답에서 이 필드가 누락되면 MissingFieldException이 발생할 수 있습니다. 특히 서버와 앱 배포 타이밍이 어긋나는 상황에서 앱 크래시 위험이 있습니다.
다만, 현재 코드베이스에서 isRegistered(KakaoLoginResponseDto), alarmEnabled(NotificationResponseDto) 등 다른 non-null Boolean 필드들도 동일한 구조로 정의되어 있으므로, 이 이슈는 aiAnswerExists만의 문제가 아니라 전체 DTO 설계 패턴의 일관성 문제입니다.
아래와 같이 기본값을 추가하거나, 서버에서 이 필드가 항상 포함됨을 보장하도록 API 계약을 명확히 해야 합니다:
방어적 수정안
`@SerialName`("aiAnswerExists")
- val aiAnswerExists: Boolean
+ val aiAnswerExists: Boolean = false📝 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.
| @SerialName("aiAnswerExists") | |
| val aiAnswerExists: Boolean | |
| `@SerialName`("aiAnswerExists") | |
| val aiAnswerExists: Boolean = false |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.kt`
around lines 26 - 27, QuestRecordedDetailResponseDto currently defines
aiAnswerExists as a non-null Boolean which will throw MissingFieldException if
the server omits the field; update the DTO to provide a safe default (e.g.,
aiAnswerExists = false) or make it nullable (Boolean?) so deserialization won't
crash, and apply the same defensive pattern to other DTOs with non-null Booleans
(e.g., KakaoLoginResponseDto.isRegistered, NotificationResponseDto.alarmEnabled)
to keep behavior consistent.
| item { | ||
| Spacer(modifier = Modifier.weight(1f)) | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete && wc -l QuestBehaviorCompleteScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 109
🏁 Script executed:
cd app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete && sed -n '195,220p' QuestBehaviorCompleteScreen.ktRepository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID
Length of output: 875
🌐 Web query:
Jetpack Compose Modifier.weight() LazyColumn LazyItemScope scope limitations
💡 Result:
Why Modifier.weight() feels “limited” in LazyColumn
1) weight() is scope-restricted
Modifier.weight() is only available for children of Row/Column, i.e. in RowScope or ColumnScope (scope-safety). It’s not a general-purpose modifier you can use anywhere (including directly in a LazyColumn item unless you’re inside a Row/Column). [1], [2]
LazyColumn { item { … } }gives you aLazyItemScope, not aColumnScope. Soweight()won’t be in scope unless you’re inside aRow {}/Column {}within that item. [2]
2) weight() can be ignored in vertically scrollable contexts
Even if you are inside a Column within a LazyColumn item, ColumnScope.weight() distributes remaining vertical space. In a vertically scrollable parent (LazyColumn), the item is typically measured with unbounded/infinite height, so “remaining height” is not a finite number—Compose effectively can’t solve the weight calculation and the weight may be ignored. [3]
Practical implications / workarounds
- Use
weight()for horizontal sizing inside an item: put content in aRow { ... }and apply weights there (width is bounded, so it behaves predictably). [1], [2] - Avoid vertical
weight()for “fill the rest of the screen” inside aLazyColumnitem unless you first give that item (or an inner container) a finite height (e.g., explicit height, or sizing relative to the viewport via Lazy item sizing APIs), then useColumn+weight()within that bounded space. The key is: weights need finite remaining constraints to distribute. [3]
Sources: Android Developers modifier scope for weight() [1], Compose scope-safety explanation [2], and why weight() fails in LazyColumn/scrollable vertical constraints [3].
[1] (developer.android.com)
[2] (medium.com)
[3] (stackoverflow.com)
Citations:
- 1: https://developer.android.com/develop/ui/compose/modifiers-list?utm_source=openai
- 2: https://medium.com/androiddevelopers/fundamentals-of-compose-layouts-and-modifiers-64d794664b66?utm_source=openai
- 3: https://stackoverflow.com/questions/77739750/why-modifier-weight-doesnt-work-in-a-lazycolumn?utm_source=openai
LazyColumn item 내 Modifier.weight(1f)는 스코프 불일치로 컴파일 오류가 발생합니다.
Modifier.weight()는 RowScope/ColumnScope 전용 API이지만, LazyColumn { item { } }는 LazyItemScope를 제공하므로 직접 호출할 수 없습니다. 컴파일 타임에 수신자 불일치 오류가 발생합니다.
제안된 Modifier.height(screenHeightDp(24.dp))는 버튼을 맨 아래로 위치시키려는 원래 의도를 해결하지 못합니다. Column으로 감싸거나 적절한 높이 제약을 설정한 후 weight()를 적용하는 것이 필요합니다.
추가로, 버튼의 onClick 핸들러가 /*Todo: ai 버튼 연결 */ 상태로 남아있으므로 기능 구현을 완료해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt`
around lines 207 - 209, The Spacer inside LazyColumn's item is using
Modifier.weight(1f) which causes a scope mismatch (weight is only available in
ColumnScope/RowScope); wrap the item content in a Column (e.g., Column(modifier
= Modifier.fillMaxHeight())) or replace the item with a container (Box/Column)
that provides ColumnScope, then move Spacer(Modifier.weight(1f)) inside that
Column so it composes correctly; also replace the placeholder /*Todo: ai 버튼 연결
*/ in the Button onClick with the real handler (call an existing ViewModel or
navigation method like viewModel.onAiButtonClicked() or a passed lambda) so the
AI button is wired up.
| Icon( | ||
| imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus), | ||
| contentDescription = null, | ||
| tint = ByeBooTheme.colors.primary300, | ||
| tint = ByeBooTheme.colors.gray500, | ||
| ) |
There was a problem hiding this comment.
업로드 아이콘에 접근성 레이블이 필요합니다.
현재 업로드 상태가 아닐 때 클릭 가능한 UI인데 대체 텍스트가 없어 스크린리더 사용자가 동작 목적을 파악하기 어렵습니다.
수정 예시 diff
+import androidx.compose.ui.res.stringResource
...
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus),
- contentDescription = null,
+ contentDescription = stringResource(R.string.quest_photo_upload),
tint = ByeBooTheme.colors.gray500,
)R.string.quest_photo_upload 문자열 리소스는 별도로 추가해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt`
around lines 89 - 93, The Icon currently sets contentDescription = null which
makes the clickable upload control inaccessible; change it to use the string
resource (R.string.quest_photo_upload) via stringResource and pass that to
contentDescription (update imports to include
androidx.compose.ui.res.stringResource), and ensure the new string resource
R.string.quest_photo_upload is added to resources; refer to the Icon usage
(ImageVector.vectorResource(id = R.drawable.ic_plus)) in QuestPhotoPicker.kt to
locate and update the contentDescription.
| fun onSaveClicked(context: Context) { | ||
| viewModelScope.launch { | ||
| _uiState.update { it.copy(isUploading = true) } | ||
|
|
||
| val state = _uiState.value | ||
| val imageUrl = state.selectedImageUri ?: return@launch | ||
| val questId = state.questId | ||
| val answer = state.questAnswer | ||
| val emotion = state.selectedEmotion?.toData().orEmpty() | ||
|
|
||
| runCatching { | ||
| val inputStream = context.contentResolver.openInputStream(imageUrl) | ||
| val imageBytes = inputStream?.readBytes() ?: error("이미지 파일을 읽을 수 없습니다.") | ||
| val contentType = context.contentResolver.getType(imageUrl).toString() | ||
| val imageKey = UUID.randomUUID().toString() | ||
|
|
||
| uploadImageUseCase( | ||
| imageBytes = imageBytes, | ||
| contentType = contentType, | ||
| imageKey = imageKey, | ||
| questId = questId, | ||
| answer = answer, | ||
| emotion = emotion, | ||
| isEditMode = false, | ||
| ).getOrThrow() | ||
| }.onSuccess { | ||
| mixpanelUtil.trackEvent( | ||
| eventName = "quest_success", | ||
| properties = | ||
| mapOf( | ||
| "quest_end_at" to getFormattedDate(), | ||
| "quest_number" to questId, | ||
| "quest_type" to "행동형", | ||
| "after_emotion_type" to emotion, | ||
| ), | ||
| ) | ||
| _uiState.update { | ||
| it.copy( | ||
| showBottomSheet = false, | ||
| showCompleteModal = true | ||
| ) | ||
| } | ||
| }.onFailure { | ||
| _sideEffect.emit( | ||
| QuestBehaviorSideEffect.ShowSnackBar("서버에 연결할 수 없습니다. 잠시 후 시도해 주세요."), | ||
| ) | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
isUploading이 고착될 수 있습니다.
Line 133 조기 반환, 실패 경로, 성공 경로에서 isUploading = false가 보장되지 않아 로딩 상태가 풀리지 않을 수 있습니다.
🔧 제안 수정안
fun onSaveClicked(context: Context) {
viewModelScope.launch {
- _uiState.update { it.copy(isUploading = true) }
-
- val state = _uiState.value
- val imageUrl = state.selectedImageUri ?: return@launch
+ val state = _uiState.value
+ val imageUrl =
+ state.selectedImageUri ?: run {
+ _sideEffect.emit(
+ QuestBehaviorSideEffect.ShowSnackBar("이미지를 선택해 주세요."),
+ )
+ return@launch
+ }
+ _uiState.update { it.copy(isUploading = true) }
val questId = state.questId
val answer = state.questAnswer
val emotion = state.selectedEmotion?.toData().orEmpty()
- runCatching {
+ runCatching {
// upload...
}.onSuccess {
// success...
}.onFailure {
_sideEffect.emit(
QuestBehaviorSideEffect.ShowSnackBar("서버에 연결할 수 없습니다. 잠시 후 시도해 주세요."),
)
}
+ _uiState.update { it.copy(isUploading = false) }
}
}📝 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.
| fun onSaveClicked(context: Context) { | |
| viewModelScope.launch { | |
| _uiState.update { it.copy(isUploading = true) } | |
| val state = _uiState.value | |
| val imageUrl = state.selectedImageUri ?: return@launch | |
| val questId = state.questId | |
| val answer = state.questAnswer | |
| val emotion = state.selectedEmotion?.toData().orEmpty() | |
| runCatching { | |
| val inputStream = context.contentResolver.openInputStream(imageUrl) | |
| val imageBytes = inputStream?.readBytes() ?: error("이미지 파일을 읽을 수 없습니다.") | |
| val contentType = context.contentResolver.getType(imageUrl).toString() | |
| val imageKey = UUID.randomUUID().toString() | |
| uploadImageUseCase( | |
| imageBytes = imageBytes, | |
| contentType = contentType, | |
| imageKey = imageKey, | |
| questId = questId, | |
| answer = answer, | |
| emotion = emotion, | |
| isEditMode = false, | |
| ).getOrThrow() | |
| }.onSuccess { | |
| mixpanelUtil.trackEvent( | |
| eventName = "quest_success", | |
| properties = | |
| mapOf( | |
| "quest_end_at" to getFormattedDate(), | |
| "quest_number" to questId, | |
| "quest_type" to "행동형", | |
| "after_emotion_type" to emotion, | |
| ), | |
| ) | |
| _uiState.update { | |
| it.copy( | |
| showBottomSheet = false, | |
| showCompleteModal = true | |
| ) | |
| } | |
| }.onFailure { | |
| _sideEffect.emit( | |
| QuestBehaviorSideEffect.ShowSnackBar("서버에 연결할 수 없습니다. 잠시 후 시도해 주세요."), | |
| ) | |
| } | |
| } | |
| fun onSaveClicked(context: Context) { | |
| viewModelScope.launch { | |
| val state = _uiState.value | |
| val imageUrl = | |
| state.selectedImageUri ?: run { | |
| _sideEffect.emit( | |
| QuestBehaviorSideEffect.ShowSnackBar("이미지를 선택해 주세요."), | |
| ) | |
| return@launch | |
| } | |
| _uiState.update { it.copy(isUploading = true) } | |
| val questId = state.questId | |
| val answer = state.questAnswer | |
| val emotion = state.selectedEmotion?.toData().orEmpty() | |
| runCatching { | |
| val inputStream = context.contentResolver.openInputStream(imageUrl) | |
| val imageBytes = inputStream?.readBytes() ?: error("이미지 파일을 읽을 수 없습니다.") | |
| val contentType = context.contentResolver.getType(imageUrl).toString() | |
| val imageKey = UUID.randomUUID().toString() | |
| uploadImageUseCase( | |
| imageBytes = imageBytes, | |
| contentType = contentType, | |
| imageKey = imageKey, | |
| questId = questId, | |
| answer = answer, | |
| emotion = emotion, | |
| isEditMode = false, | |
| ).getOrThrow() | |
| }.onSuccess { | |
| mixpanelUtil.trackEvent( | |
| eventName = "quest_success", | |
| properties = | |
| mapOf( | |
| "quest_end_at" to getFormattedDate(), | |
| "quest_number" to questId, | |
| "quest_type" to "행동형", | |
| "after_emotion_type" to emotion, | |
| ), | |
| ) | |
| _uiState.update { | |
| it.copy( | |
| showBottomSheet = false, | |
| showCompleteModal = true | |
| ) | |
| } | |
| }.onFailure { | |
| _sideEffect.emit( | |
| QuestBehaviorSideEffect.ShowSnackBar("서버에 연결할 수 없습니다. 잠시 후 시도해 주세요."), | |
| ) | |
| } | |
| _uiState.update { it.copy(isUploading = false) } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt`
around lines 128 - 176, onSaveClicked can leave _uiState.isUploading stuck true
because the early return (when state.selectedImageUri is null) and the
runCatching success/failure paths don't always reset it; fix by ensuring
_uiState.update { it.copy(isUploading = false) } runs on every exit: set
isUploading = true at start of onSaveClicked, then use a try { ... } finally {
_uiState.update { it.copy(isUploading = false) } } around the image
upload/runCatching logic (or explicitly update in the early-return path and both
onSuccess/onFailure handlers) so that the UI always clears the loading state
(refer to onSaveClicked, _uiState, runCatching/uploadImageUseCase).
| val inputStream = context.contentResolver.openInputStream(imageUrl) | ||
| val imageBytes = inputStream?.readBytes() ?: error("이미지 파일을 읽을 수 없습니다.") | ||
| val contentType = context.contentResolver.getType(imageUrl).toString() | ||
| val imageKey = UUID.randomUUID().toString() |
There was a problem hiding this comment.
이미지 스트림을 닫지 않아 리소스 누수 위험이 있습니다.
openInputStream() 결과를 use {}로 닫아야 하고, MIME 타입도 toString() 대신 null 검증이 필요합니다.
🔧 제안 수정안
- val inputStream = context.contentResolver.openInputStream(imageUrl)
- val imageBytes = inputStream?.readBytes() ?: error("이미지 파일을 읽을 수 없습니다.")
- val contentType = context.contentResolver.getType(imageUrl).toString()
+ val imageBytes =
+ context.contentResolver.openInputStream(imageUrl)?.use { it.readBytes() }
+ ?: error("이미지 파일을 읽을 수 없습니다.")
+ val contentType =
+ context.contentResolver.getType(imageUrl)
+ ?: error("이미지 MIME 타입을 확인할 수 없습니다.")Also applies to: 210-213
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt`
around lines 139 - 142, In QuestBehaviorViewModel, the image input stream opened
with context.contentResolver.openInputStream(imageUrl) must be closed to avoid
resource leaks and the MIME type must be null-checked instead of calling
toString(); change the code that reads inputStream, imageBytes, contentType and
imageKey to wrap the InputStream in a use { } block (e.g., inputStream?.use {
... } ) so it is auto-closed and handle a null inputStream by throwing or
returning an error, and replace
context.contentResolver.getType(imageUrl).toString() with a null-safe check
(e.g., val contentType = context.contentResolver.getType(imageUrl) ?: error("이미지
MIME 타입을 결정할 수 없습니다.")) — apply the same pattern to the other occurrence around
lines 210-213.
| val imageKey = | ||
| requireNotNull(state.imageKey) { | ||
| "It must have imageKey" | ||
| } | ||
| val result = | ||
| questBehaviorRepository.updateQuestBehavior( | ||
| questId = questId, | ||
| request = | ||
| QuestBehaviorEditModel( | ||
| answer = state.questAnswer, | ||
| imageKey = imageKey, | ||
| ), |
There was a problem hiding this comment.
imageKey 검증이 null만 확인해서 빈 문자열이 통과됩니다.
초기 상태에서 imageKeyArg.orEmpty()를 사용하고 있어, Line 337의 requireNotNull은 사실상 방어가 되지 않습니다. 빈 문자열까지 차단해야 안전합니다.
🔧 제안 수정안
- val imageKey =
- requireNotNull(state.imageKey) {
- "It must have imageKey"
- }
+ val imageKey =
+ state.imageKey?.takeIf { it.isNotBlank() } ?: run {
+ _sideEffect.emit(
+ QuestBehaviorSideEffect.ShowSnackBar("이미지 정보가 없어 수정할 수 없습니다."),
+ )
+ return@launch
+ }📝 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.
| val imageKey = | |
| requireNotNull(state.imageKey) { | |
| "It must have imageKey" | |
| } | |
| val result = | |
| questBehaviorRepository.updateQuestBehavior( | |
| questId = questId, | |
| request = | |
| QuestBehaviorEditModel( | |
| answer = state.questAnswer, | |
| imageKey = imageKey, | |
| ), | |
| val imageKey = | |
| state.imageKey?.takeIf { it.isNotBlank() } ?: run { | |
| _sideEffect.emit( | |
| QuestBehaviorSideEffect.ShowSnackBar("이미지 정보가 없어 수정할 수 없습니다."), | |
| ) | |
| return@launch | |
| } | |
| val result = | |
| questBehaviorRepository.updateQuestBehavior( | |
| questId = questId, | |
| request = | |
| QuestBehaviorEditModel( | |
| answer = state.questAnswer, | |
| imageKey = imageKey, | |
| ), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt`
around lines 336 - 347, The current null check uses
requireNotNull(state.imageKey) which allows empty strings; update the validation
of imageKey in the block that builds QuestBehaviorEditModel (the variable
imageKey used before calling questBehaviorRepository.updateQuestBehavior) to
reject blank/empty values as well (e.g., require that state.imageKey is neither
null nor blank) so an empty imageKeyArg.orEmpty() cannot slip through; adjust
the require call or add an additional check on imageKey.isNotBlank() and throw a
clear error if invalid.
Related issue 🛠
Work Description 📝
Screenshot 📸
default.mp4
default.mp4
Uncompleted Tasks 😅
PR Point 📌
트러블 슈팅 💥
Summary by CodeRabbit
Release Notes
New Features
UI/UX Improvements
Refactoring