Skip to content

[feat/#267] 퀘스트 작성 UI 변경 및 추가, 스크롤 추가#275

Open
znayeonzn wants to merge 18 commits intodevelopfrom
feat/#267-recording-quest-ui
Open

[feat/#267] 퀘스트 작성 UI 변경 및 추가, 스크롤 추가#275
znayeonzn wants to merge 18 commits intodevelopfrom
feat/#267-recording-quest-ui

Conversation

@znayeonzn
Copy link
Collaborator

@znayeonzn znayeonzn commented Feb 22, 2026

Related issue 🛠

Work Description 📝

  • 퀘스트 작성 스크린, 퀘스트 완료 스크린 UI 수정
  • TextField 입력 시, 스크롤 기능 추가
  • 작성 완료 후, 퀘스트 완료 모달 띄우기
  • 공통 퀘스트 UI 추가

Screenshot 📸

default.mp4
default.mp4
공통퀘스트UI

Uncompleted Tasks 😅

  • 공통 퀘스트 네비 연결
  • 공통 퀘스트 모달, 완료 팝업 카드 띄우기

PR Point 📌

  • 공통 퀘스트 네비는 아직 완벽히 연결하진 못했습니다.
  • 공통 퀘스트 작성 부분에서 모달은 소희 언니 모달 컴포넌트 받아온 후에 진행하겠습니다 ~
  • 퀘스트 작성이 공기계에서는 기능 명세대로 작동하는데 애뮬레이터에서는 이상하게 뜨더라고요! 참고해주세여 (도와주신 소희언니에게 절을 올립니다 🙇‍♀️🙇‍♀️)

트러블 슈팅 💥

Summary by CodeRabbit

Release Notes

  • New Features

    • Added quest common writing flow with simplified text input interface
    • Introduced reusable quest writing components including header bar, footer counter, and title section
    • Added quest behavior writing feature with photo attachment and text input capabilities
  • UI/UX Improvements

    • Enhanced quest writing screens with improved layout and scrollable content areas
    • Added character count display for quest answers with visual feedback
    • Improved photo picker responsiveness and layout
    • Added quest completion modals with confirmation dialogs
  • Refactoring

    • Reorganized quest module package structure for better maintainability
    • Restructured state management for quest writing workflows
    • Updated navigation routing for quest common and recording flows

@znayeonzn znayeonzn self-assigned this Feb 22, 2026
@znayeonzn znayeonzn added 🍒 [FEAT] 새로운 기능 구현 🍥 [UI] UI 작업 labels Feb 22, 2026
@znayeonzn znayeonzn added the 🐰 아연 아연 label Feb 22, 2026
@auto-assign auto-assign bot requested review from fredleeJH and sohee6989 February 22, 2026 09:54
@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Walkthrough

퀘스트 작성 UI를 세분화된 패키지 구조로 재구조화하고, 기록형·행동형·공통 퀘스트별 전용 작성 화면을 도입했습니다. 공유 UI 컴포넌트(TopBar, Footer, Title)를 추가하고, QuestTextField를 단순화하며, 새로운 QuestCommonRoute/ViewModel을 통해 공통 퀘스트 흐름을 추가했습니다.

Changes

Cohort / File(s) Summary
퀘스트 작성 UI 컴포넌트
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/component/text/QuestWritingTitle.kt
새로운 공유 UI 컴포넌트 추가: 뒤로가기/완료 버튼이 있는 TopBar, 문자 수 제한 표시 Footer, 질문 텍스트 및 TIP 태그를 표시하는 Title 컴포넌트.
QuestTextField 개편
app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt
QuestWritingState 제거, BasicTextField 기반으로 단순화, 포커스 자동 스크롤 추가, 공개 API 서명 변경(scrollState 매개변수 추가).
행동형 퀘스트 작성 흐름
app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/*
새로운 QuestBehaviorViewModel 추가(403줄, 이미지 업로드 및 완료 모달 처리), QuestBehaviorWritingScreen 신규 작성(356줄), QuestBehaviorState를 계산 속성 기반으로 개편.
행동형 퀘스트 완료 흐름
app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/*
QuestBehaviorCompleteScreen 신규 추가(237줄, Coil 이미지 캐시 정책 DISABLED), 상태/ViewModel 패키지 이동 및 selectedEmotion을 non-nullable로 변경.
기록형 퀘스트 작성 흐름
app/src/main/java/com/byeboo/app/presentation/quest/record/writing/*
QuestRecordingScreen 신규 위치로 이동(251줄, 완료 모달 타임아웃 2초 처리), QuestRecordingState를 계산 속성 기반으로 개편, ViewModel에서 onCompleteClicked() 추가 및 showCompleteModal 도입.
기록형 퀘스트 완료 흐름
app/src/main/java/com/byeboo/app/presentation/quest/record/complete/*
QuestRecordingCompleteScreen 개편(LazyColumn → Column with verticalScroll, 상단 동작 버튼 추가), 패키지 이동으로 인한 import 조정.
공통 퀘스트 신규 흐름
app/src/main/java/com/byeboo/app/presentation/quest/common/*
QuestCommonRoute/Screen/ViewModel/State 신규 추가(149줄 화면, 44줄 ViewModel, 27줄 상태), 500자 제한 텍스트 입력 및 네비게이션 통합.
공통 퀘스트 네비게이션
app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.kt, app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonRoute.kt
navigateToQuestCommon(), navigateToQuestCommonComplete(), questCommonGraph() 함수 추가, QuestCommonWriting/QuestCommonComplete 라우트 클래스 도입.
퀘스트 네비게이션 통합
app/src/main/java/com/byeboo/app/presentation/quest/navigation/QuestNavigation.kt, app/src/main/java/com/byeboo/app/presentation/quest/record/navigation/*, app/src/main/java/com/byeboo/app/presentation/quest/behavior/navigation/*
QuestRecord 라우트명 변경(QuestRecording → QuestRecordingWriting), 공통 퀘스트 콜백 추가, 패키지 이동에 따른 import 경로 수정.
메인 네비게이션 업데이트
app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt, app/src/main/java/com/byeboo/app/presentation/main/MainNavigator.kt
navigateToQuestCommon() 및 navigateToQuestCommonComplete() 네비게이션 헬퍼 메서드 추가.
퀘스트 화면 확장
app/src/main/java/com/byeboo/app/presentation/quest/QuestScreen.kt, app/src/main/java/com/byeboo/app/presentation/quest/screen/CommonJourneyScreen.kt
navigateToQuestCommon 콜백 추가, CommonJourneyScreen에 onCommonQuestClick 연결.
행동형 컴포넌트 수정
app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt
fillMaxWidth 레이아웃으로 변경, 고정 너비 제거, 아이콘 색상 primary300 → gray500 업데이트.
퀘스트 완료 다이얼로그
app/src/main/java/com/byeboo/app/presentation/quest/component/card/QuestCompleteDialog.kt
QuestCompleteDialog 공개 래퍼 추가, QuestCompleteCard를 private으로 변경, Dialog 비활성화 속성 설정.
데이터 모델 확장
app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.kt, app/src/main/java/com/byeboo/app/domain/model/quest/QuestRecordedDetailModel.kt, app/src/main/java/com/byeboo/app/data/mapper/quest/QuestRecordedDetailMapper.kt
aiAnswerExists 필드 추가(응답 DTO, 도메인 모델, 매퍼 업데이트).

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
Loading
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()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #258: QuestBehaviorState/ViewModel과 작성 화면/라우트의 버튼 활성화, fromOffboarding 네비게이션 인자 등 행동형 퀘스트 작성 흐름 변경 사항이 겹침.
  • #34: QuestBehaviorWritingScreen, QuestBehaviorCompleteScreen, QuestBehaviorViewModel, 포토 피커, 바텀 시트, 네비게이션 등 동일한 행동형 퀘스트 UI 컴포넌트 수정.
  • #120: 퀘스트 관련 화면(QuestRecording/QuestRecordingComplete/QuestReview 등) 재구조화 및 라우트 분리로 동일한 구조 변경 패턴 포함.

Suggested reviewers

  • sohee6989
  • fredleeJH

🐰 세 가지 길이 하나의 숲이 되어
글쓰기, 기록, 행동의 무대가 다시 선 채로
공통 퀘스트 새 빛 맞이하네
완료 모달이 2초 춤을 춘다

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경 사항인 퀘스트 작성 UI 변경과 스크롤 추가를 명확히 요약하고 있습니다.
Description check ✅ Passed PR 설명은 필수 섹션(Related issue, Work Description, Screenshots, Uncompleted Tasks, PR Point, 트러블 슈팅)을 포함하고 있으며 대부분 완성되어 있습니다.
Linked Issues check ✅ Passed PR 코드는 이슈 #267의 요구사항인 UI 수정, 스크롤 기능 추가, 공통 퀘스트 UI 추가를 모두 구현하고 있습니다.
Out of Scope Changes check ✅ Passed 모든 코드 변경은 이슈 #267의 요구사항(퀘스트 작성 UI 변경, 스크롤 추가, 공통 퀘스트 추가)과 관련되어 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#267-recording-quest-ui

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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, questRecordedDetailRepositoryval로 선언되어 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.

Comment on lines 167 to 168
val scrollState = rememberScrollState()

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

scrollStateLazyColumn과 연결되지 않아 "타이핑 시 자동 스크롤" 기능이 동작하지 않음

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

questWritingState: QuestWritingState,
value: String,
onValueChange: (String) -> Unit,
scrollState: ScrollState,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find QuestTextField usage and verticalScroll usage to verify the claim
rg -n "QuestTextField" --type=kt -B 3 -A 5

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

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

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 2822


🏁 Script executed:

#!/bin/bash
# Find all Kotlin files
find . -name "*.kt" -type f | head -20

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 1613


🏁 Script executed:

#!/bin/bash
# Search for QuestTextField usage
rg "QuestTextField" -B 3 -A 5

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 5892


🏁 Script executed:

#!/bin/bash
# Search for verticalScroll usage
rg "verticalScroll" -B 5 -A 5

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

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

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 14725


QuestBehaviorWritingScreen에서 scrollState가 verticalScroll과 연결되지 않음

QuestBehaviorWritingScreen은 scrollState를 생성하여 QuestTextField에 전달하지만, 부모 컨테이너에서 verticalScroll(scrollState)를 적용하지 않습니다. 대신 LazyColumn을 사용하고 있어서, QuestTextField 내부의 animateScrollTo 호출이 실제 스크롤 효과를 발생시키지 못합니다.

QuestRecordingScreen은 올바르게 ColumnverticalScroll(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.

Comment on lines 60 to 69
decorationBox = { innerTextField ->
if (value.isEmpty() && !(focusState.value)) {
Text(
text = placeholder,
color = ByeBooTheme.colors.gray300,
style = ByeBooTheme.typography.body3,
)
}
innerTextField()
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

decorationBoxBox 래퍼 없음 — placeholder가 innerTextField 위에 오버레이되지 않고 수직으로 쌓임

value.isEmpty() && !focusState.value 조건이 참일 때, Text(placeholder)innerTextField()Box 없이 순차적으로 호출되면 두 컴포저블이 겹치지 않고 세로로 배치되어 플레이스홀더와 텍스트 입력 영역이 이중으로 나타납니다.

플레이스홀더 텍스트를 올바르게 표시하려면 innerTextFieldBox로 감싸고 플레이스홀더를 조건부로 표시해야 합니다.

🐛 제안 수정
+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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

SubcomposeAsyncImageerror 상태 처리 누락.

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번 줄의 ShowInAppReview emit은 구독자 없이 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일 때 isUploadingtrue로 고정되는 버그

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: decorationBoxBox 래퍼가 없어 placeholder와 텍스트 필드가 세로로 쌓임

이전 리뷰에서 지적된 것과 동일한 문제입니다. placeholder TextinnerTextField()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: selectedEmotion nullable 타입과 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가 생성만 되고 사용되지 않음

bringIntoViewRequesterremember로 생성되었지만, 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: advancedImePaddingcomposed {} 대신 Modifier.Node 기반 구현을 고려

composed {}는 각 사용처마다 새로운 컴포지션을 생성하여 성능 오버헤드가 있습니다. Compose 최신 권장 사항에서는 Modifier.Node API를 사용하는 것을 권장합니다. 현재 단일 사용이므로 즉각적인 문제는 아니지만, 참고해 두시면 좋겠습니다.

🤖 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.kt Line 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: 외부 BoxImageUploadButton의 크기/클리핑이 중복됨

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이 계산만 되고 사용되지 않음

lastLineBottomonTextLayout에서 값이 설정되지만, 이 컴포저블 내에서도 외부에서도 참조되지 않습니다. 이전 구현에서 남은 데드 코드로 보이며, 제거해도 됩니다.

♻️ 데드 코드 제거 제안
     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

📥 Commits

Reviewing files that changed from the base of the PR and between d6c40a3 and 18244dd.

📒 Files selected for processing (11)
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/QuestBehaviorWritingScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/navigation/QuestBehaviorNavigation.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt
  • app/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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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 -100

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

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

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

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

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

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

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

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

@znayeonzn znayeonzn changed the title [feat/#267] 기록형 퀘스트 작성 UI 변경 및 스크롤 추가 [feat/#267] 퀘스트 작성 UI 변경 및 추가, 스크롤 추가 Feb 24, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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도 동일하게 modifierModifier로 변경하세요.

🤖 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 } 콜백을 모두 제거하세요. OptionalSectiononFocusChanged 파라미터도 해당 호출자에서 제거할 수 있습니다.

♻️ 수정 제안
-val isFocused = remember { mutableStateOf(false) }
             if (event.key.nativeKeyCode == KeyEvent.KEYCODE_BACK) {
                 focusManager.clearFocus(force = true)
-                isFocused.value = false
                 true
         OptionalSection(
             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

📥 Commits

Reviewing files that changed from the base of the PR and between 18244dd and df06ebf.

📒 Files selected for processing (14)
  • app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt
  • app/src/main/java/com/byeboo/app/presentation/main/MainNavigator.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/QuestScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonRoute.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingTitle.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/model/QuestState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/navigation/QuestNavigation.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt
  • app/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

Comment on lines +161 to +162
val density = LocalDensity.current
val isImeVisible = WindowInsets.ime.getBottom(density) > 0
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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
fi

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

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

Repository: 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 reading getBottom() 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:


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.

Comment on lines 32 to 47
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
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 2

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

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

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

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

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

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

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

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

Comment on lines +38 to +56
@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
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

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

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

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

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

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

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 792


QuestCommonRoute에서 네비게이션 콜백이 사용되지 않음 - 수정 필수

검증 결과, 지적된 문제가 확인되었습니다:

  1. QuestCommonRoute에 side-effect 수집 로직 부재: LaunchedEffect가 없어 ViewModel의 sideEffect를 구독하지 않음
  2. ViewModel의 네비게이션 핸들러가 비어있음: onBackClicked() (line 24-26)와 onCompleteClicked() (line 28-30)이 아무 구현이 없음
  3. QuestCommonSideEffect 정의 불완전: NavigateToQuest만 정의되어 있고 NavigateToQuestCommonComplete/NavigateUp 이벤트 누락
  4. 전달받은 콜백이 사용되지 않음: 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.

Comment on lines +24 to +30
fun onBackClicked() {

}

fun onCompleteClicked() {

}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

뒤로가기/완료 핸들러가 비어 있어 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.

Comment on lines +221 to +233
QuestTextField(
value = uiState.questAnswer,
onValueChange = {
if (it.length <= 500) {
onUpdateContent(isFocused.value, it)
}
},
placeholder = "글로 적다 보면, 스스로에게 한 걸음 더 가까워질 수 있어요.",
onFocusChanged = {
isFocused.value = it
},
scrollState = scrollState,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +145 to 146
onClick = { onCommonQuestClick(0) },
buttonText = "답변 작성하기",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Copy link
Collaborator

@fredleeJH fredleeJH left a comment

Choose a reason for hiding this comment

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

고생 많으셨습니다! 확인 한 번 부탁드립니다~~

Modifier
.fillMaxWidth()
.padding(horizontal = screenWidthDp(48.dp)),
dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분 이전 PR에서 소희님이 QuestQuitModal 정의 시 dialogProperties를 디폴트 인자로 넣어두었습니다. 별도로 설정을 변경하실 게 아니라면 호출부에서는 생략 하셔도 될거 같습니다! 머지하실 때 참고 부탁드립니다~

Copy link
Collaborator

Choose a reason for hiding this comment

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

저 오늘 모달 수정해놓은 피알 머지해놔갖구 그거 pull 당겨서 해주시면 될 것 같아요!!
화이팅~!!

isPhotoQuestion = true,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 14.dp)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
.padding(bottom = 14.dp)
.padding(bottom = screenHeightDp(14.dp))

Text(
text = "(${imageCount}/1)",
color = ByeBooTheme.colors.gray400,
style = ByeBooTheme.typography.body5,
Copy link
Collaborator

Choose a reason for hiding this comment

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

body6 같습니다!

}
},
placeholder = "꼭 적지 않아도 괜찮지만, 글로 정리해 보면 스스로에게 한 걸음 더 가까워질 수 있어요.",
onFocusChanged = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

그냥 이렇게 축약해도 되지 않을까요?

Suggested change
onFocusChanged = {
onFocusChanged = onFocusChanged

import com.byeboo.app.domain.model.quest.QuestWritingState

@Immutable
data class QuestCommonState(
Copy link
Collaborator

Choose a reason for hiding this comment

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

버튼 활성화 값을 뷰모델에서 매번 계산해서 넣어주는 대신, 데이터 클래스 안에서 자동으로 계산되게 하면 어떨까요?

Suggested change
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(
Copy link
Collaborator

Choose a reason for hiding this comment

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

위에서 제안 드린 대로 state를 작성하면 이런식으로 쓸 수 있을 것 같습니다!

Suggested change
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

그렇게 하면 이 부분은 그냥 지워도 될 것 같습니다!


append(text = maxCharCount.toString())
},
style = ByeBooTheme.typography.body6,
Copy link
Collaborator

Choose a reason for hiding this comment

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

cap2 인 것 같습니다!


Spacer(modifier = Modifier.height(screenHeightDp(12.dp)))

Text(
Copy link
Collaborator

Choose a reason for hiding this comment

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

gray50, head2 확인 부탁드립니다!

Copy link
Collaborator

@sohee6989 sohee6989 left a comment

Choose a reason for hiding this comment

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

일단 요거 먼저 남길게요!!!ㅎㅎ

@@ -1,4 +1,4 @@
package com.byeboo.app.presentation.quest.behavior
package com.byeboo.app.presentation.quest.behavior.complete
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 패키지 분리하니 넘 깔끔하네요!!!

modifier =
modifier
.width(screenWidthDp(96.dp))
.width(screenWidthDp(312.dp))
Copy link
Collaborator

Choose a reason for hiding this comment

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

p1: 이거 기기대응 해주려면 가로 너비를 고정값을 주면 안 될 것 같아요..!!
fillMaxWidth로 주고 가로 패딩 나중에 상위에서 주고 aspectRatio 이거 활용하면 되지 않을까 싶은데 수정 부탁드립니다!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

!! 감사하빈다!!

Modifier
.fillMaxWidth()
.padding(horizontal = screenWidthDp(48.dp)),
dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
Copy link
Collaborator

Choose a reason for hiding this comment

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

저 오늘 모달 수정해놓은 피알 머지해놔갖구 그거 pull 당겨서 해주시면 될 것 같아요!!
화이팅~!!

sealed class QuestCommonRoute : Route {

@Serializable
data class QuestCommon(
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3: 이거 다른 화면들처럼 작성 화면은 ~ writing으로 하는거 어떠세요??
그래서 이건 QuestCommonWriting으로 하고 관련 화면들 이름도 이런식으로요!!

modifier =
modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
Copy link
Collaborator

Choose a reason for hiding this comment

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

p1: 여기 지금 가로 패딩이 상위에서 중복으로 들어가고 있습니다!!
상위 Column에서 한 번에 24 가로 패딩을 주고 있으니 여기 빼도 될 것 같아요!

},
style = ByeBooTheme.typography.body6,
color = ByeBooTheme.colors.gray400,
textAlign = TextAlign.End,
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3: Text가 wrapContent 상태라 textAlign = TextAlign.End는 실제로는 동작하지 않고 있습니다!
현재 Spacer(weight = 1f)로 이미 우측 정렬이 되어 있어서 제거해도 동일하게 동작해 없애도 될 것 같아요!!

import com.byeboo.app.core.util.screenWidthDp

@Composable
fun QuestWritingTopBar(
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3: 개인적인 건의사항..ㅎㅎ
모달처럼 이거 탑바 저희 basicTopBar 하나 만들고 QuestWritingTopBar 이것처럼 만들어서 쓰는거 어떠세요???
작성화면만 공통 탑바 만들지 말고 작성완료 화면도 탑바 동일하니 공통으로 만들어서 쓰는 방향으로요!!

그렇게 하면서 패키지도 component/topbar 안에 위치시키면 좋을 것 같아요!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

아님 요거 전체적으로 다 건드려야 되니까 바쁘시면 제가 할게여!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

갓 소 희
이것도 혹시 미뤄봐도 될까요?^___^

onCompleteClick: () -> Unit,
) {
Row(
modifier = Modifier.padding(horizontal = screenWidthDp(22.dp)),
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2: 공통 컴포넌트이니 modifier 뚫어놓는거 어떠세요?/

Spacer(modifier = Modifier.height(screenHeightDp(16.dp)))
ByeBooButton(
onClick = { /* TODO : 답변 작성 화면 이동 */ },
onClick = { onCommonQuestClick(0) },
Copy link
Collaborator

Choose a reason for hiding this comment

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

p1: 이거 questId 자체로 넣어놔야 되는거 아닌가여..????

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ㅋㅋㅋㅋ 이거 네비만 확인해보려고 그냥 임시로 넣어놨어요.... 리드님이 해결해주시면 정말 좋겟ㅆ다..제발...한번만₩~~

Copy link
Collaborator

@sohee6989 sohee6989 left a comment

Choose a reason for hiding this comment

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

까다로운 작업 수고하셨습니다!!
리뷰 확인 부탁드립니다아~~

Comment on lines 111 to 126
Dialog(
onDismissRequest = {},
properties =
DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = false,
dismissOnClickOutside = false,
),
) {
QuestCompleteCard(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = screenWidthDp(24.dp)),
)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2: QuestCompleteCard에 이 Dialog까지 넣어줘도 될 것 같아요!!

Spacer(modifier = Modifier.height(screenHeightDp(16.dp)))
Spacer(modifier = Modifier.height(screenHeightDp(24.dp)))

LazyColumn(
Copy link
Collaborator

@sohee6989 sohee6989 Feb 25, 2026

Choose a reason for hiding this comment

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

p1: 지금 item 하나에 Column을 사용해서 화면 전체를 그리고 있는데 그러면 LazyColumn을 쓰는 의미가 없는 것 같습니다..!!
지금 상태면 그냥 Column에 verticalscroll 주는게 더 나은것 같다고 생각이 드는데 의견이 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

소희님 의견에 동의합니다!
이번 스프린트부터 UI를 컴포넌트화 해주어서 기존에 쓰던 LazyColumn 보다는 verticalscroll 이 낫다고 생각해요!
수정하겠습니다!!

buttonTextColor = ByeBooTheme.colors.white,
buttonStyle = ByeBooTheme.typography.body2,
buttonBackgroundColor = ByeBooTheme.colors.primary300,
onClick = {},
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2: 요거 onClick 빈 람다로 두는 것 대신 TODO로 남겨놓든가 상위 호이스팅 부탁드릴게요!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

p1: 이 화면에는 보리에게 답장 받기 버튼 안 해놓으셨어요!

Comment on lines 22 to 25
questNumber: Long? = null,
question: String,
onTipClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
Copy link
Collaborator

Choose a reason for hiding this comment

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

p1: 파라미터 순서 지켜주세요~~

style = ByeBooTheme.typography.body2,
)

Spacer(modifier = modifier.width(screenWidthDp(8.dp)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2: 이거 대신에 Row에서 horizontalArrangement = Arrangement.spacedBy(screenWidthDp(8.dp)) 어떠세요??

)
}

Spacer(modifier = modifier.height(screenHeightDp(12.dp)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2: 이것도 Column에서 주는거 어떠세요??

textStyle = ByeBooTheme.typography.cap1,
)

Spacer(modifier = modifier.width(screenWidthDp(8.dp)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2: Row에서 주는거 어ㄸ세요?

)
}

Spacer(modifier = modifier.height(screenHeightDp(8.dp)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2: Column에서 주는거 어떠세요??

Comment on lines -135 to -139
keyboardActions =
KeyboardActions(onDone = {
keyboardController?.hide()
focusManager.clearFocus()
}),
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2: keyboardActions 이거 넣어주는거 어떠세요?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (6)
app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt (1)

77-86: ⚠️ Potential issue | 🟡 Minor

decorationBoxBox 래퍼 필요 — placeholder가 수직으로 쌓임

placeholderinnerTextField()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 | 🟡 Minor

500자 초과 붙여넣기 시 전체 입력이 무시됨

현재 구현에서 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이라 클릭 이벤트가 전부 무시됩니다. sideEffect emit으로 연결하거나, 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 215 onClick이 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 시 재시작되지 않지만, showCompleteModalfalse에서 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

📥 Commits

Reviewing files that changed from the base of the PR and between df06ebf and 84bfc02.

📒 Files selected for processing (28)
  • app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.kt
  • app/src/main/java/com/byeboo/app/data/mapper/quest/QuestRecordedDetailMapper.kt
  • app/src/main/java/com/byeboo/app/domain/model/quest/QuestRecordedDetailModel.kt
  • app/src/main/java/com/byeboo/app/presentation/main/MainNavHost.kt
  • app/src/main/java/com/byeboo/app/presentation/main/MainNavigator.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/QuestScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete/QuestBehaviorCompleteState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/component/QuestPhotoPicker.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/behavior/writing/QuestBehaviorWritingScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonNavigation.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/navigation/QuestCommonRoute.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonState.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/common/writing/QuestCommonViewModel.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/QuestWritingTopbar.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/card/QuestCompleteDialog.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingFooter.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/text/QuestWritingTitle.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/component/text/textfield/QuestTextField.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/record/complete/QuestRecordingCompleteScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/record/navigation/QuestRecordNavigation.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/record/navigation/QuestRecordRoute.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingScreen.kt
  • app/src/main/java/com/byeboo/app/presentation/quest/record/writing/QuestRecordingState.kt
  • app/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

Comment on lines +26 to +27
@SerialName("aiAnswerExists")
val aiAnswerExists: Boolean
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# 대상 파일 확인 및 전체 구조 파악
find . -path ./app/src/main/java/com/byeboo/app/data/dto/response/quest/QuestRecordedDetailResponseDto.kt

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

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

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 1070


🏁 Script executed:

# aiAnswerExists 필드 관련된 모든 코드 검색
rg -n "aiAnswerExists" --type=kt -B2 -A2

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 101


🏁 Script executed:

# QuestRecordedDetailResponseDto 사용 패턴 확인
rg -n "QuestRecordedDetailResponseDto" --type=kt -B1 -A3

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 101


🏁 Script executed:

# 응답 샘플 또는 테스트 데이터 확인
rg -n '"aiAnswerExists"' --type=json --type=kt -B2 -A2

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 101


🏁 Script executed:

# 비슷한 DTO의 기본값 패턴 확인
fd "ResponseDto.kt$" -x rg -l "val.*Boolean" {} | head -5

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

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

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 57


🏁 Script executed:

# Retrofit 또는 API 호출 부분에서 QuestRecordedDetailResponseDto 사용 패턴
find . -name "*.kt" -exec grep -l "QuestRecordedDetailResponseDto" {} \; | head -5

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

Repository: 36-APPJAM-HEARTZ/ByeBoo-ANDROID

Length of output: 59


🏁 Script executed:

# QuestRecordedDetailService 구현 확인
cat app/src/main/java/com/byeboo/app/data/service/quest/QuestRecordedDetailService.kt

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

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

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

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

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

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

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

Suggested change
@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.

Comment on lines +207 to +209
item {
Spacer(modifier = Modifier.weight(1f))

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd app/src/main/java/com/byeboo/app/presentation/quest/behavior/complete && wc -l QuestBehaviorCompleteScreen.kt

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

Repository: 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 a LazyItemScope, not a ColumnScope. So weight() won’t be in scope unless you’re inside a Row {} / 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 a Row { ... } and apply weights there (width is bounded, so it behaves predictably). [1], [2]
  • Avoid vertical weight() for “fill the rest of the screen” inside a LazyColumn item 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 use Column + 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:


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.

Comment on lines 89 to 93
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus),
contentDescription = null,
tint = ByeBooTheme.colors.primary300,
tint = ByeBooTheme.colors.gray500,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

업로드 아이콘에 접근성 레이블이 필요합니다.

현재 업로드 상태가 아닐 때 클릭 가능한 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.

Comment on lines +128 to +176
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("서버에 연결할 수 없습니다. 잠시 후 시도해 주세요."),
)
}

}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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).

Comment on lines +139 to +142
val inputStream = context.contentResolver.openInputStream(imageUrl)
val imageBytes = inputStream?.readBytes() ?: error("이미지 파일을 읽을 수 없습니다.")
val contentType = context.contentResolver.getType(imageUrl).toString()
val imageKey = UUID.randomUUID().toString()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

이미지 스트림을 닫지 않아 리소스 누수 위험이 있습니다.
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.

Comment on lines +336 to +347
val imageKey =
requireNotNull(state.imageKey) {
"It must have imageKey"
}
val result =
questBehaviorRepository.updateQuestBehavior(
questId = questId,
request =
QuestBehaviorEditModel(
answer = state.questAnswer,
imageKey = imageKey,
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

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

Labels

🍒 [FEAT] 새로운 기능 구현 🍥 [UI] UI 작업 🐰 아연 아연

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[feat] 기록형, 행동형, 공통 퀘스트 작성 UI

3 participants