Skip to content

Comments

문제 상세 화면 구현#34

Merged
chanho0908 merged 29 commits intomainfrom
33-문제-상세-화면questiondetailscreen-구현
Jan 17, 2026

Hidden character warning

The head ref may contain hidden characters: "33-\ubb38\uc81c-\uc0c1\uc138-\ud654\uba74questiondetailscreen-\uad6c\ud604"
Merged

문제 상세 화면 구현#34
chanho0908 merged 29 commits intomainfrom
33-문제-상세-화면questiondetailscreen-구현

Conversation

@chanho0908
Copy link
Owner

@chanho0908 chanho0908 commented Jan 17, 2026

이슈 번호

Closes #33

작업내용

문제 상세 화면(QuestionDetailScreen)을 완전히 구현했습니다.

주요 구현 내용:

1. 도메인 및 데이터 레이어

  • Answer 도메인 모델 구현 (Current/History sealed class)
  • AnswerRepository 및 RemoteAnswerDataSource 구현
  • 답변 생성/수정/삭제 API 연동

2. UI 컴포넌트

  • QuestionDetailScreen: 문제 상세 화면 메인
  • QuestionInfo: 문제 정보 표시 컴포넌트
  • MyAnswer: 현재 작성한 답변 표시/수정
  • AnswerHistory: 과거 답변 이력 표시
  • AddAnswerBottomSheet: 새 답변 작성 바텀시트
  • AnswerCard/EditAnswerCard/EmptyAnswerCard: 답변 카드 컴포넌트

3. Design System 개선

  • ConfirmDialog 추가
  • AppTextArea 개선
  • CategoryExtensions 추가

4. 상태 관리 및 Navigation

  • QuestionDetailViewModel 구현
  • SavedStateHandle을 활용한 화면 간 상태 동기화
    • 상세 화면에서 변경된 좋아요/해결 상태가 리스트 화면에 자동 반영
  • QuestionDetailNavGraph 구성

5. 유틸리티

  • DateFormatter 추가 (날짜 포맷팅)

chanho0908 and others added 26 commits January 16, 2026 18:16
- question → questions/list 이동 (Question 관련 파일들)
- data/model → data/model/request, data/model/response 구조화
Answer 모델을 sealed class로 설계하여 타입 안전성 강화

- Answer.Current: 현재 답변 (answers_current)
- Answer.History: 히스토리 답변 (answer_history)
- AnswerRepository 인터페이스 추가
- kotlinx-datetime 의존성 추가

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
Supabase API 응답을 위한 Response 모델 구현

- CurrentAnswerResponse: answers_current 테이블 응답
- AnswerHistoryResponse: answer_history 테이블 응답
- Answer sealed class로 변환하는 toDomain() 메서드 추가

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
답변 CRUD를 위한 DataSource 구현

**Request 모델:**
- CreateAnswerRequest: 답변 생성 요청
- UpdateAnswerRequest: 답변 수정 요청
- RpcDefaultRequest: RPC 호출 공통 요청

**DataSource 구현:**
- RemoteAnswerDataSource: 인터페이스
- DefaultRemoteAnswerDataSource: Supabase 구현체
  - 답변 생성 시 기존 답변 히스토리 이동
  - 답변 삭제 시 최신 히스토리 복원
  - 단일 책임 원칙 준수 (헬퍼 함수 분리)

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
Repository 구현 및 Koin 의존성 주입 설정

- DefaultAnswerRepository: AnswerRepository 구현체
- DataSourceModule: RemoteAnswerDataSource 등록
- RepositoryModule: AnswerRepository 등록

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
Instant를 yyyy.MM.dd 형식으로 변환하는 유틸리티

- formatDate(): Instant → "2024.01.15" 변환
- 월/일을 2자리로 패딩하여 일관된 형식 제공

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
UI Layer의 타입 안전한 답변 모델 구현

- AnswerUiModel.Current: 현재 답변 UI 모델
- AnswerUiModel.History: 히스토리 답변 UI 모델
- toUiModel(): Domain Answer → UI AnswerUiModel 변환
- Preview Provider 추가 (Preview 데이터 제공)

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
문제 상세 화면의 UI 상태 관리 모델

- QuestionDetailUiState: currentAnswer와 historyAnswers 분리
- QuestionDetailUiEvent: UI 이벤트 정의
- draftAnswer 상태로 바텀시트 입력 내용 보존
- QuestionDetailPreviewParameterProvider 추가

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
문제 상세 화면의 비즈니스 로직 처리

**주요 기능:**
- 답변 조회 및 상태 관리
- 답변 추가/수정/삭제 (타입 안전한 sealed class 사용)
- draftAnswer 상태 관리 (바텀시트 입력 보존)
- 문제 해결 상태 자동 업데이트
- 답변 저장 시 자동 trim 처리

**타입 안전성:**
- onDeleteAnswer(AnswerUiModel): sealed class when 표현식
- onUpdateAnswer(AnswerUiModel.Current, String): 타입 명시

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
재사용 가능한 디자인 시스템 컴포넌트 추가

**신규 컴포넌트:**
- ConfirmDialog: 확인 다이얼로그 (아이콘, 제목, 메시지, 버튼)
- Modifier.conditionalBorder: 조건부 border 적용 확장 함수

**개선사항:**
- AppTextArea: minLines 파라미터 추가 (기본 3줄)
- AppPrimaryButton: enabled 상태 지원
- Dimen: textFieldHeightLarge 추가

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
답변 표시를 위한 기본 카드 컴포넌트

**AnswerCard:**
- sealed class 타입에 따라 날짜 표시 분기 (when 표현식)
- Current: "마지막 수정: yyyy.MM.dd"
- History: "yyyy.MM.dd"
- 수정/삭제 버튼 옵션

**EmptyAnswerCard:**
- 답변이 없을 때 표시하는 빈 카드
- 안내 메시지 제공

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
답변 수정을 위한 인라인 편집 카드

- AppTextArea 사용한 답변 편집
- 저장/취소 버튼
- 현재 답변 수정 시 히스토리에 추가되지 않고 업데이트

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
내 답변 섹션 컴포넌트

**타입 안전성:**
- answer: AnswerUiModel.Current? (nullable)
- onUpdateAnswer: (AnswerUiModel.Current, String) - 타입 명시
- onDeleteAnswer: (AnswerUiModel.Current) - 타입 명시

**기능:**
- 답변 없음: EmptyAnswerCard 표시
- 일반 모드: AnswerCard 표시
- 수정 모드: EditAnswerCard 표시
- 삭제 확인 다이얼로그

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
답변 히스토리 섹션 컴포넌트

**타입 안전성:**
- historyAnswers: ImmutableList<AnswerUiModel.History>
- onDeleteAnswer: (AnswerUiModel.History) - 타입 명시
- AnswerCard 재사용 (onEdit = null로 읽기 전용)

**기능:**
- 히스토리 개수 표시
- 타임라인 시각화
- 각 히스토리 삭제 가능
- 히스토리는 읽기 전용 (수정 불가)

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
답변 추가를 위한 바텀시트 컴포넌트

**특징:**
- ModalBottomSheet 사용
- draftAnswer 상태로 입력 내용 보존
- 취소 시에도 draft 유지
- 저장 시에만 draft 초기화
- 키보드 대응 (imePadding, navigationBarsPadding)

**UX 개선:**
- 실수로 닫아도 작성 내용 유지
- 재오픈 시 이전 내용 표시

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
문제 정보를 표시하는 컴포넌트

- 문제 제목 표시
- 카테고리 뱃지
- 해결 상태 뱃지
- CategoryExtensions 활용한 카테고리 색상

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
문제 상세 화면 메인 컴포저블

**주요 구성:**
- QuestionInfo: 문제 정보
- MyAnswer: 내 답변 섹션
- AnswerHistory: 히스토리 섹션
- AddAnswerBottomSheet: 답변 추가
- 즐겨찾기 토글 버튼

**타입 안전성:**
- onUpdateAnswer: (AnswerUiModel.Current, String)
- onDeleteAnswer: (AnswerUiModel)
- sealed class when 표현식으로 타입 분기

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
문제 상세 화면 네비게이션 구성

- QuestionDetailNavGraph: 상세 화면 네비게이션 그래프
- NavRoutes.QuestionDetail 추가
- MainNavGraphContributor에 통합
- Type-safe navigation 적용

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
Koin 의존성 주입 모듈 설정

- ViewModelModule: QuestionDetailViewModel 등록
- NavigationModule: QuestionDetailNavGraph 등록
- parameterViewModelOf 사용한 파라미터 주입

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
문제 상세 화면에 필요한 문자열 리소스

- 답변 관련 문자열 (내 답변, 답변 없음, 플레이스홀더)
- 버튼 텍스트 (저장, 취소, 수정, 삭제)
- 다이얼로그 메시지
- 답변 히스토리 텍스트

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
문제 목록에서 상세 화면으로 이동 구현

- QuestionList: 아이템 클릭 시 상세 화면 이동
- QuestionViewModel: NavigateToDetail 이벤트 추가
- QuestionUiEvent.NavigateToDetail 정의
- MainScreen: 이벤트 핸들러 연결

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
패키지 구조 변경으로 인한 테스트 코드 수정

- FakeRemoteQuestionDataSource: response 패키지 경로 수정
- QuestionResponseFixture: response 패키지 경로 수정
- UI 테스트 문서 추가

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
QuestionDetail 기능 구현을 위한 빌드 설정

- kotlinx-datetime 의존성 추가
- Napier 로깅 라이브러리 추가
- Gradle 버전 카탈로그 업데이트

🎯 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
@chanho0908 chanho0908 linked an issue Jan 17, 2026 that may be closed by this pull request
34 tasks
@coderabbitai
Copy link

coderabbitai bot commented Jan 17, 2026

📝 Walkthrough

Walkthrough

문제 상세 화면(QuestionDetailScreen) 기능을 전체 레이어에 걸쳐 추가했습니다. UI 구성요소(QuestionDetailScreen, QuestionInfo, MyAnswer, AnswerCard, AnswerHistory, AddAnswerBottomSheet 등)와 미리보기용 Provider들을 도입하고, QuestionDetailViewModel로 상태·이벤트를 관리합니다. 네비게이션에는 QuestionDetailGraph 및 NavRoutes 직렬화가 추가되었고 MainScreen/QuestionScreen 쪽과 SavedStateHandle을 통해 결과를 전달합니다. 도메인에는 Answer 모델·AnswerRepository 인터페이스를 추가하고, data 레이어에 RemoteAnswerDataSource/DefaultRemoteAnswerDataSource 및 DefaultAnswerRepository를 구현해 API 연동을 확장했습니다. 디자인 시스템에는 ConfirmDialog, AppTextArea 개선 및 토큰 조정이 포함됩니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.30% 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 제목 '문제 상세 화면 구현'은 주요 변경사항인 QuestionDetailScreen 구현을 명확히 요약하고 있습니다.
Description check ✅ Passed PR 설명에서 이슈 번호, 주요 구현 내용, 레이어별 변경사항이 구체적으로 제시되어 있어 변경사항과 관련이 있습니다.
Linked Issues check ✅ Passed 이슈 #33의 모든 주요 목표가 충족됩니다: UI 구조(QuestionDetailScreen, 헤더, 문제 정보, 내 답변, 히스토리, 액션), 상태 관리(ViewModel, SavedStateHandle), 기능(수정/삭제/추가/토글), 네비게이션(NavRoutes, 라우팅), 스타일링(ConfirmDialog, 컴포넌트) 모두 구현됨.
Out of Scope Changes check ✅ Passed 모든 변경사항이 QuestionDetailScreen 구현과 관련 있습니다. 패키지 구조 개선(question → questions.list), 디자인 시스템 개선(AppButtonDefaults, AppTextArea, ConfirmDialog), 유틸리티(DateFormatter) 추가 등은 기능 구현을 지원하는 필수 변경입니다.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/AppTextArea.kt (1)

51-92: minLines 파라미터 검증과 라인 높이를 동적으로 계산하는 것을 권장합니다.

minLines가 0이나 음수로 전달되면 최소 높이가 0 이하가 될 수 있는 문제가 있습니다. 또한 24.dp는 Material Design 3의 bodyLarge 라인 높이에 맞춰진 하드코딩된 값이라, 나중에 타이포그래피가 변경되면 동기화가 깨질 수 있습니다.

다음과 같이 개선하면 안전합니다:

  • minLines.coerceAtLeast(1)로 최소값을 보장하기
  • MaterialTheme.typography.bodyLarge의 실제 lineHeight를 기반으로 최소 높이 계산하기
🔧 제안 수정안
 import androidx.compose.foundation.layout.heightIn
+import androidx.compose.ui.platform.LocalDensity

@@
 fun AppTextArea(
@@
     maxCharacters: Int? = null,
     minLines: Int = 3,
@@
 ) {
     val interactionSource = remember { MutableInteractionSource() }
     val isFocused by interactionSource.collectIsFocusedAsState()
+
+    val baseTextStyle = MaterialTheme.typography.bodyLarge
+    val textStyle = baseTextStyle.copy(color = MaterialTheme.colorScheme.onSurface)
+    val lineHeight = with(LocalDensity.current) {
+        val lh = if (baseTextStyle.lineHeight.isUnspecified) baseTextStyle.fontSize else baseTextStyle.lineHeight
+        lh.toDp()
+    }
+    val minHeight = lineHeight * minLines.coerceAtLeast(1)

     val borderColor by animateColorAsState(
@@
         BasicTextField(
@@
-            textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
+            textStyle = textStyle,
@@
             modifier =
                 Modifier
                     .fillMaxWidth()
-                    .heightIn(min = (minLines * 24).dp)
+                    .heightIn(min = minHeight)
🤖 Fix all issues with AI agents
In `@composeApp/src/commonMain/composeResources/values/strings.xml`:
- Around line 38-40: Remove the duplicate/unused string resources: delete
last_modified and last_modified_format and the unused date_suffix_year and
date_suffix_month entries from strings.xml, keep last_modified_prefix as
AnswerCard.kt uses it, and search for any remaining references to last_modified
/ last_modified_format to update callers to use last_modified_prefix (or compose
the localized format where needed); also confirm DateFormatter.kt behavior (it
currently uses "." as separator) and either switch it to use a resource if
localization is required or leave it hardcoded but ensure no removed resources
are referenced.

In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/common/util/DateFormatter.kt`:
- Around line 3-6: Replace the import of kotlinx.datetime.Instant with
kotlin.time.Instant in DateFormatter (change the line importing Instant to use
kotlin.time.Instant) while leaving the other kotlinx.datetime imports (TimeZone,
toLocalDateTime, number) unchanged so the kotlinx.datetime extension APIs
continue to work with the new kotlin.time.Instant type; update any references
that explicitly qualify the old type to use kotlin.time.Instant where needed.

In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/navigation/NavRoutes.kt`:
- Around line 6-9: The sealed class NavRoutes is annotated `@Serializable` but
many of its direct subclasses (e.g., data object LoginGraph, Login, MainGraph,
Main, Test, History, Profile, QuestionDetailGraph) are missing `@Serializable`;
update each direct subclass of NavRoutes to include the `@Serializable` annotation
(add `@Serializable` above each data object declaration) so the entire sealed
hierarchy (NavRoutes and all its direct child objects) is explicitly
serializable and consistent with kotlinx.serialization requirements.

In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/AnswerCard.kt`:
- Around line 91-116: Change the IconButton touch target to meet the
Material-accessibility minimum by removing or replacing
Modifier.size(Dimen.iconLg) on the IconButton instances (the ones using onEdit
and onDelete) so the buttons use the default 48dp touch target or explicitly use
Modifier.size(Dimen.touchTargetMin); keep the inner Icon using
Modifier.size(Dimen.iconSm) so only the visible icon remains small while the
button's touch area is 48dp, and ensure tint and other props remain unchanged.

In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/AnswerHistory.kt`:
- Around line 66-76: The UI currently renders the count twice: once via
stringResource(Res.string.answer_history_count, historyAnswers.size) and again
via the hardcoded Text "(${historyAnswers.size})"; update the implementation so
only one localized string renders the count—either remove the second Text call
that hardcodes parentheses or change the string resource used by AnswerHistory
to include a numeric placeholder (e.g., "%d" or locale-appropriate format) and
pass historyAnswers.size into stringResource; ensure any remaining label/number
split uses separate localized strings (e.g., label and number) rather than
hardcoded punctuation so i18n is preserved.
- Around line 85-90: 현재 historyAnswers.forEachIndexed로 렌더링하면서 키를 지정하지 않아
HistoryItem 내부의 remember 상태(showDeleteConfirm 등)가 위치 기반으로 이동하는 문제가 발생합니다; 해결하려면
forEachIndexed 블록 안에서 각 항목을 고유 ID로 고정하는 key(answer.id) 래퍼로 HistoryItem을 감싸서
Compose가 각 HistoryItem을 answer.id로 추적하게 하세요 (참조: historyAnswers.forEachIndexed,
HistoryItem, answer.id, showDeleteConfirm).

In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/EditAnswerCard.kt`:
- Line 22: 현재 EditAnswerCard.kt의 import
androidx.compose.ui.tooling.preview.Preview는 Android 전용이라 commonMain에서 컴파일 오류를
유발합니다; EditAnswerCard.kt에서 해당 Android 전용 import를 제거하고 대신
org.jetbrains.compose.ui.tooling.preview.Preview를 사용하도록 import를 변경(Preview
어노테이션을 사용하는 모든 위치에 영향을 주는 부분)하고, Preview 어노테이션을 사용하는 함수들(예: EditAnswerCard 미리보기
함수)을 그대로 유지해 org.jetbrains.compose의 Preview를 참조하도록 업데이트하세요.

In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/EmptyAnswerCard.kt`:
- Line 12: The `@Preview` import and annotation in EmptyAnswerCard.kt are
Android-only and break multiplatform builds; remove the
androidx.compose.ui.tooling.preview.Preview import and any `@Preview` annotations
from the commonMain EmptyAnswerCard composable, and instead place a preview-only
composable in the androidMain source set (or use an expect/actual wrapper) so
that EmptyAnswerCard (the composable function) remains in commonMain while
preview code lives in androidMain.

In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/QuestionDetailScreen.kt`:
- Around line 32-33: The commonMain module imports and uses Android-only Compose
tooling (Preview, PreviewParameter and `@Preview` on the composable in
QuestionDetailScreen.kt and the PreviewParameterProvider classes), which breaks
non-Android builds; move all `@Preview-annotated` composable functions (the
Preview on QuestionDetailScreen at the lines where `@Preview` is used) and the
four PreviewParameterProvider classes from composeApp/src/commonMain/... into
composeApp/src/androidMain/... preserving package names, remove the
androidx.compose.ui.tooling.preview imports from QuestionDetailScreen.kt in
commonMain, and ensure the preview-only files in androidMain import
androidx.compose.ui.tooling.preview.Preview and PreviewParameter as needed so
only Android builds include tooling APIs.

In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/vm/QuestionDetailViewModel.kt`:
- Around line 50-63: The loadAnswers() call swallows network failures via
getOrNull() and getOrElse { emptyList() }, making UI indistinguishable between
"no data" and "error"; modify loadAnswers() to handle failures explicitly by
using Result.onFailure/onSuccess (or check Result.isFailure) for
answerRepository.fetchCurrentAnswer(questionId) and
fetchAnswerHistory(questionId), and update the _uiState (same state updated by
_uiState.update) to carry an error flag/message (or trigger the same
snackbar/error path used by onToggleFavorite()) on failure so the UI can surface
network errors instead of silently showing empty data.
- Around line 34-47: The loadQuestionDetail() flow can leave _uiState in a
perpetual loading state if fetchQuestions() fails or if the requested questionId
isn't found; update loadQuestionDetail() to add an onFailure handler for
questionRepository.fetchQuestions() that sets _uiState.update {
it.loading(false) } (and logs or surfaces the error), and ensure the onSuccess
path also clears loading when question == null by calling _uiState.update {
it.loading(false) } or setting an error state; reference functions/fields:
loadQuestionDetail(), questionRepository.fetchQuestions(), _uiState.update,
loadAnswers(), and UiState.updateAnswers()/loading so the fix is applied where
the success/failure handling occurs.

In
`@data/src/commonMain/kotlin/com/peto/droidmorning/data/model/request/UpdateAnswerRequest.kt`:
- Around line 6-10: UpdateAnswerRequest uses `@SerialName`("content") while
CreateAnswerRequest and some RPC requests use the "p_" prefix (e.g.,
`@SerialName`("p_content")), causing inconsistent API field names; confirm whether
UpdateAnswerRequest targets the same backend endpoint as CreateAnswerRequest and
if so update UpdateAnswerRequest to use `@SerialName`("p_content") to match
CreateAnswerRequest (or vice versa if the agreed convention is no prefix),
otherwise add a clear comment above the UpdateAnswerRequest data class
documenting why it intentionally differs (reference the UpdateAnswerRequest and
CreateAnswerRequest classes and related LikeRequest / RpcDefaultRequest patterns
when making the change).

In
`@data/src/commonMain/kotlin/com/peto/droidmorning/data/model/response/CurrentAnswerResponse.kt`:
- Line 4: Replace the deprecated kotlinx.datetime.Instant import in
CurrentAnswerResponse with kotlin.time.Instant and update any uses of Instant in
the `@Serializable` data class CurrentAnswerResponse to the stdlib type (ensure
you remove or update any kotlinx.datetime-specific extensions/usages); if you
rely on serialization, ensure kotlinx-serialization is >=1.9.0 and either use
the built-in kotlin.time Instant serializer or annotate the property with the
appropriate serializer, and confirm your Kotlin version is >=2.1.20 (preferably
>=2.3.0).

In
`@designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/ConfirmDialog.kt`:
- Line 27: The imported Preview is Android-only: replace the Android-specific
import androidx.compose.ui.tooling.preview.Preview with the Compose
Multiplatform tooling-preview import
org.jetbrains.compose.ui.tooling.preview.Preview in ConfirmDialog.kt (update the
import statement that references Preview so the file in commonMain compiles for
iOS/Desktop/Android).

In `@domain/build.gradle.kts`:
- Line 27: Remove the unused kotlinx.datetime dependency by deleting the
implementation(libs.kotlinx.datetime) declaration from the domain module's
Gradle config; the domain models Question.kt and Answer.kt (and tests) use
kotlin.time.Instant from the standard library, so no external datetime
dependency is needed—ensure no other code references kotlinx.datetime before
committing.
♻️ Duplicate comments (1)
designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/ConfirmDialog.kt (1)

145-161: Preview 코드는 androidMain으로 이동이 필요합니다

위에서 언급한 import 문제와 연결하여, 이 Preview 함수 전체를 androidMain 소스셋의 해당 경로로 이동해야 합니다. ConfirmDialogContent가 private이므로 Preview에서 접근하려면 internal로 변경하거나, Preview 전용 래퍼를 만드는 방법을 고려해 보세요.

🧹 Nitpick comments (21)
composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/list/component/CategoryChips.kt (1)

38-66: 반복적인 칩 생성 패턴에 대한 선택적 개선 제안입니다.

현재 구현은 명시적이고 이해하기 쉽습니다. 다만, Category enum이 확장될 경우를 대비해 Category.entries를 순회하는 방식으로 리팩토링을 고려해 볼 수 있습니다. 이를 위해 Category에 대한 displayName 확장 프로퍼티를 정의하면 됩니다.

현재 방식의 장점은 각 카테고리와 문자열 리소스 매핑이 명시적이어서 컴파일 타임에 누락된 리소스를 감지할 수 있다는 점입니다. 새로운 카테고리가 자주 추가될 예정이 아니라면 현재 구현을 유지해도 무방합니다.

designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/ConfirmDialog.kt (1)

88-102: 아이콘 크기를 상수로 추출하면 일관성 유지에 도움됩니다

현재 72.dp40.dp가 하드코딩되어 있습니다. 다른 spacing 값들이 Dimen 상수를 사용하는 것처럼, 아이콘 관련 크기도 상수로 추출하면 디자인 시스템 전체의 일관성을 유지하기 좋습니다.

또한 androidx.compose.foundation.shape.CircleShape를 inline으로 사용하는 대신 상단에 import하면 코드가 더 깔끔해집니다.

♻️ 제안하는 개선 코드
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.shape.CircleShape
 ...

+private object ConfirmDialogDefaults {
+    val iconContainerSize = 72.dp
+    val iconSize = 40.dp
+}

             Box(
                 modifier =
                     Modifier
-                        .size(72.dp)
-                        .clip(androidx.compose.foundation.shape.CircleShape)
+                        .size(ConfirmDialogDefaults.iconContainerSize)
+                        .clip(CircleShape)
                         .background(iconBackgroundColor),
                 contentAlignment = Alignment.Center,
             ) {
                 Icon(
                     imageVector = icon,
                     contentDescription = null,
                     tint = iconTint,
-                    modifier = Modifier.size(40.dp),
+                    modifier = Modifier.size(ConfirmDialogDefaults.iconSize),
                 )
             }
data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt (1)

13-15: 예외 처리 방식에 대한 고려 사항입니다.

현재 uid() 함수에서 사용자가 로그인하지 않은 경우 IllegalStateException을 던지고 있습니다. 이 방식도 유효하지만, 코딩 가이드라인에 따르면 "에러가 도메인 레벨로 적절히 변환되는가?"를 고려해야 합니다.

도메인 수준의 에러 타입(예: NotAuthenticatedException)으로 변환하면 상위 레이어에서 더 명확한 에러 처리가 가능합니다. 현재 구조에서 Repository 레이어에서 이러한 변환을 담당하고 있다면 현재 구현도 괜찮습니다.

data/src/commonTest/kotlin/com/peto/droidmorning/data/fixture/QuestionResponseFixture.kt (1)

8-25: 테스트 픽스처가 잘 구성되어 있습니다. 다만 유연성 개선을 제안드립니다.

isLikedisSolved가 항상 true로 고정되어 있습니다. 다른 필드들처럼 기본값을 제공하면서도 파라미터로 받을 수 있게 하면, 다양한 테스트 시나리오(예: 좋아요 안 한 질문, 미해결 질문)를 더 쉽게 구성할 수 있습니다.

♻️ 제안하는 개선 코드
 fun questionResponse(
     id: Long = 1L,
     title: String = "테스트 질문",
     category: Category = Category.Android,
     sourceUrl: String = "https://example.com",
     createdAt: Instant = Instant.parse("2024-01-01T00:00:00Z"),
     updatedAt: Instant = Instant.parse("2024-01-01T00:00:00Z"),
+    isLiked: Boolean = true,
+    isSolved: Boolean = true,
 ): QuestionResponse =
     QuestionResponse(
         id = id,
         title = title,
         category = category.name,
         sourceUrl = sourceUrl,
         createdAt = createdAt,
         updatedAt = updatedAt,
-        isLiked = true,
-        isSolved = true,
+        isLiked = isLiked,
+        isSolved = isSolved,
     )
composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/EmptyAnswerCard.kt (1)

19-40: 컴포넌트 구현이 깔끔합니다.

  • MaterialTheme의 색상, 타이포그래피, 도형을 일관되게 사용하고 있습니다.
  • Dimen.spacing2xl 디자인 토큰 사용이 좋습니다.
  • stringResource를 통한 다국어 지원도 잘 되어 있습니다.

작은 제안으로, alpha = 0.3f 값을 디자인 시스템에 상수로 정의하면 일관성 유지에 도움이 될 수 있습니다. 다만 이는 선택적 개선 사항입니다.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/QuestionInfo.kt (2)

63-68: 접근성(Accessibility) 개선: Icon의 contentDescription 제공을 권장합니다.

현재 contentDescription = null로 설정되어 있어 스크린 리더 사용자가 아이콘의 의미를 파악하기 어렵습니다. Text와 함께 사용되어 장식적 요소로 볼 수도 있지만, 명시적인 설명을 제공하면 접근성이 향상됩니다.

♻️ 접근성 개선 제안
 Icon(
     imageVector = Icons.Filled.Check,
-    contentDescription = null,
+    contentDescription = stringResource(Res.string.answer_completed),
     tint = MaterialTheme.colorScheme.primary,
     modifier = Modifier.size(Dimen.iconXs),
 )

또는 Rowsemantics { contentDescription = ... }를 적용하여 전체를 하나의 접근성 요소로 처리할 수도 있습니다.


87-97: Preview 커버리지 확장을 고려해 보세요.

현재 isSolved = true 상태만 Preview로 제공됩니다. isSolved = false인 경우의 Preview도 추가하면 다양한 상태를 개발 중 빠르게 확인할 수 있습니다.

♻️ 미해결 상태 Preview 추가 제안
`@Preview`(showBackground = true)
`@Composable`
private fun QuestionInfoUnsolvedPreview() {
    AppTheme {
        QuestionInfo(
            title = "Coroutine의 Dispatcher 종류에 대해 설명해주세요.",
            category = Category.Coroutine,
            isSolved = false,
        )
    }
}
composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/list/vm/QuestionViewModel.kt (1)

94-98: 토글 실패가 사용자에게 조용히 넘어가도 괜찮은지 확인해 주세요.
실패가 전달되지 않으면 상태가 롤백되더라도 이유를 알기 어렵습니다. 필요하다면 토스트/로그/재시도 정책을 추가하는 방향이 좋겠습니다. 의도된 UX인가요?

composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/AddAnswerBottomSheet.kt (2)

110-124: onSave 호출 시 trim()을 적용하는 것을 고려해 주세요.

현재 enabled 체크에서는 draftAnswer.trim().isNotEmpty()를 사용하지만, onSave에는 원본 draftAnswer가 전달됩니다. 이로 인해 앞뒤 공백이 포함된 상태로 저장될 수 있습니다.

♻️ 제안된 수정
 TextButton(
     onClick = {
-        if (draftAnswer.trim().isNotEmpty()) {
+        val trimmedAnswer = draftAnswer.trim()
+        if (trimmedAnswer.isNotEmpty()) {
             scope.launch {
-                onSave(draftAnswer)
+                onSave(trimmedAnswer)
                 sheetState.hide()
                 onDismiss()
             }
         }
     },
     enabled = draftAnswer.trim().isNotEmpty(),
 )

41-59: Public wrapper 함수의 필요성을 검토해 주세요.

AddAnswerBottomSheetAddAnswerBottomSheetContent를 그대로 위임하고 있어서, 이 레이어가 필요한지 궁금합니다. 향후 확장을 위한 것이라면 괜찮지만, 현재로서는 AddAnswerBottomSheetContent를 직접 public으로 만드는 것이 더 간결할 수 있습니다.

의도된 설계라면 무시하셔도 됩니다.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/model/QuestionDetailUiState.kt (1)

31-34: 간결한 표현을 고려해 주세요 (선택 사항).

toggleFavorite() 함수에서 중간 변수 currentQuestion은 생략 가능합니다. 다만 현재 코드도 가독성이 좋으므로 선택적 개선 사항입니다.

♻️ 간결한 버전
 fun toggleFavorite(): QuestionDetailUiState {
-    val currentQuestion = question
-    return copy(question = currentQuestion.copy(isLiked = !currentQuestion.isLiked))
+    return copy(question = question.copy(isLiked = !question.isLiked))
 }
composeApp/src/commonMain/composeResources/values/strings.xml (1)

44-45: 중복된 문자열 리소스가 있습니다.

delete_answer_confirm_messagedelete_answer_confirm이 동일한 텍스트("정말 이 답변을 삭제하시겠습니까?")를 가지고 있습니다. 둘 중 하나를 제거하거나, 의도적으로 구분된 것이라면 용도에 맞게 텍스트를 다르게 수정하는 것이 좋겠습니다.

♻️ 제안된 수정
 <!-- Delete Confirmation -->
 <string name="delete_answer_title">답변 삭제</string>
-<string name="delete_answer_confirm_message">정말 이 답변을 삭제하시겠습니까?</string>
-<string name="delete_answer_confirm">정말 이 답변을 삭제하시겠습니까?</string>
+<string name="delete_answer_confirm_message">정말 이 답변을 삭제하시겠습니까?</string>
 <string name="delete_history_confirm">이 답변을 삭제하시겠습니까?</string>
composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/EditAnswerCard.kt (1)

86-98: Preview에서 상태 업데이트가 연결되지 않았습니다.

mutableStateOf로 상태를 생성했지만 onContentChange에서 상태를 업데이트하지 않아 Interactive Preview에서 입력이 동작하지 않습니다. 의도적인 것이라면 remembermutableStateOf를 제거해도 됩니다.

♻️ 인터랙티브 프리뷰로 수정하는 방법
 `@Preview`
 `@Composable`
 private fun EditAnswerCardPreview() {
     var content by remember { mutableStateOf("") }
     AppTheme {
         EditAnswerCard(
             content = content,
-            onContentChange = { },
+            onContentChange = { content = it },
             onSave = {},
             onCancel = {},
         )
     }
 }
composeApp/src/commonMain/kotlin/com/peto/droidmorning/di/NavigationModule.kt (1)

12-14: 네이밍 컨벤션 일관성을 고려해 주세요.

기존 바인딩은 소문자("login", "main")를 사용하지만, 새로 추가된 바인딩은 PascalCase("QuestionDetail")를 사용합니다. 일관성을 위해 동일한 컨벤션을 적용하는 것이 좋습니다.

♻️ 네이밍 일관성 적용 제안
     single<NavGraphContributor>(named("login")) { LoginNavGraphContributor() }
     single<NavGraphContributor>(named("main")) { MainNavGraphContributor() }
-    single<NavGraphContributor>(named("QuestionDetail")) { QuestionDetailNavGraph() }
+    single<NavGraphContributor>(named("questionDetail")) { QuestionDetailNavGraph() }
data/src/commonMain/kotlin/com/peto/droidmorning/data/model/response/AnswerHistoryResponse.kt (1)

19-26: Instant.parse()에서 예외 발생 가능성이 있습니다.

API 응답의 createdAt 형식이 ISO-8601 표준과 다르거나 잘못된 경우 Instant.parse()에서 예외가 발생할 수 있습니다. 데이터 레이어에서 예외를 도메인 레벨 에러로 변환하거나, Repository에서 안전하게 처리하는 것을 권장합니다.

API 응답 형식이 항상 올바르다고 보장되나요? 아니면 방어적 처리가 필요할까요?

♻️ 방어적 처리 예시
fun toDomain(): Answer.History =
    Answer.History(
        id = id,
        userId = userId,
        questionId = questionId,
        content = content,
        createdAt = runCatching { Instant.parse(createdAt) }
            .getOrElse { Instant.DISTANT_PAST }, // 또는 적절한 기본값/에러 처리
    )

또는 Repository 레이어에서 try-catch로 감싸고 에러를 도메인 레벨로 변환하는 방법도 있습니다.

data/src/commonMain/kotlin/com/peto/droidmorning/data/model/response/CurrentAnswerResponse.kt (1)

18-24: Instant.parse 호출 시 예외 처리를 고려해 주세요.

Instant.parse(updatedAt)는 API에서 잘못된 날짜 형식이 반환될 경우 예외를 발생시킵니다. DefaultAnswerRepository에서 runCatching으로 감싸고 있어 예외가 전파되지는 않지만, 파싱 실패 시 더 명확한 에러 메시지를 제공하고 싶다면 이 레벨에서 처리하는 것도 방법입니다.

현재 구현도 동작에는 문제가 없으므로, 필요에 따라 개선 여부를 결정해 주세요.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/list/QuestionScreen.kt (1)

52-71: LaunchedEffect 내 StateFlow 수집 방식 검토

현재 LaunchedEffect에서 StateFlow.collect를 사용하고 있는데, 이 패턴은 savedStateHandle이 변경되지 않는 한 무한히 수집을 계속합니다. 상세 화면에서 돌아올 때 한 번만 값을 읽으면 되므로, collectLatest를 사용하거나 단순히 동기적으로 값을 읽는 방식도 고려해볼 수 있습니다.

다만, 현재 구현도 동작에는 문제가 없으며 값을 처리한 후 즉시 초기화하는 점(KEY_QUESTION_ID = -1L)은 좋은 패턴입니다.

♻️ 대안: 단순화된 상태 읽기 방식
 LaunchedEffect(savedStateHandle) {
-    savedStateHandle
-        ?.getStateFlow(QuestionDetailNavGraph.KEY_QUESTION_ID, -1L)
-        ?.collect { questionId ->
-            if (questionId != -1L) {
-                val isLiked = savedStateHandle.get<Boolean>(QuestionDetailNavGraph.KEY_IS_LIKED) ?: false
-                val isSolved = savedStateHandle.get<Boolean>(QuestionDetailNavGraph.KEY_IS_SOLVED) ?: false
-
-                viewModel.updateQuestionFromDetail(
-                    questionId = questionId,
-                    isLiked = isLiked,
-                    isSolved = isSolved,
-                )
-
-                savedStateHandle[QuestionDetailNavGraph.KEY_QUESTION_ID] = -1L
-                savedStateHandle.remove<Boolean>(QuestionDetailNavGraph.KEY_IS_LIKED)
-                savedStateHandle.remove<Boolean>(QuestionDetailNavGraph.KEY_IS_SOLVED)
-            }
-        }
+    val questionId = savedStateHandle?.get<Long>(QuestionDetailNavGraph.KEY_QUESTION_ID) ?: -1L
+    if (questionId != -1L) {
+        val isLiked = savedStateHandle?.get<Boolean>(QuestionDetailNavGraph.KEY_IS_LIKED) ?: false
+        val isSolved = savedStateHandle?.get<Boolean>(QuestionDetailNavGraph.KEY_IS_SOLVED) ?: false
+
+        viewModel.updateQuestionFromDetail(
+            questionId = questionId,
+            isLiked = isLiked,
+            isSolved = isSolved,
+        )
+
+        savedStateHandle?.set(QuestionDetailNavGraph.KEY_QUESTION_ID, -1L)
+        savedStateHandle?.remove<Boolean>(QuestionDetailNavGraph.KEY_IS_LIKED)
+        savedStateHandle?.remove<Boolean>(QuestionDetailNavGraph.KEY_IS_SOLVED)
+    }
 }
composeApp/src/commonMain/kotlin/com/peto/droidmorning/navigation/NavRoutes.kt (1)

26-35: QuestionDetail 라우트 정의 검토

ROUTE 상수와 createRoute() 헬퍼 함수를 함께 제공하여 타입 안전성과 유연성을 모두 확보한 점이 좋습니다.

한 가지 고려사항: QuestionDetail 클래스 자체가 @Serializable이고 Navigation Compose의 type-safe navigation을 사용한다면, ROUTEcreateRoute()는 레거시 호환성을 위해서만 필요합니다. 새로운 코드에서는 타입 안전한 방식을 우선 사용하는 것이 권장됩니다.

data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultAnswerRepository.kt (1)

7-41: Repository 구현 검토: Domain 인터페이스 구현 확인

AnswerRepository 인터페이스를 충실히 구현하고 있으며, 모든 원격 호출을 runCatching으로 감싸 일관된 에러 처리를 제공합니다. toDomain() 매퍼를 사용하여 DTO → Domain 변환 책임을 명확히 분리한 점도 좋습니다.

코딩 가이드라인에 따른 검토:

  • ✅ Domain 인터페이스 정확히 구현
  • ✅ Remote 데이터 소스 조합 로직 명확
  • ⚠️ 에러의 도메인 레벨 변환은 현재 generic Exception 그대로 전달

필요시 도메인별 에러 타입(예: NetworkError, NotFoundError)으로 변환하는 것을 고려해볼 수 있습니다.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/main/MainScreen.kt (1)

31-34: SavedStateHandle 전달 패턴 검토

SavedStateHandle을 Composable 함수의 파라미터로 전달하는 것은 일반적인 패턴이 아닙니다. 보통 SavedStateHandle은 ViewModel 내부에서 접근하거나, Navigation Component를 통해 자동으로 제공됩니다.

현재 구현이 동작하긴 하지만, 향후 유지보수 시 혼란을 줄 수 있습니다. 대안으로:

  1. QuestionViewModel에서 직접 SavedStateHandle에 접근
  2. Navigation의 result API 사용 (navController.currentBackStackEntry?.savedStateHandle)

현재 접근 방식을 선택한 이유가 있다면(예: 테스트 용이성) 주석으로 남겨두면 좋겠습니다.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/vm/QuestionDetailViewModel.kt (1)

99-127: 중첩 launch로 순서/취소 전파가 흐려질 수 있어요.
왜: onSuccess 내부에서 다시 viewModelScope.launch를 열면 코루틴 계층이 깊어지고, 실패/취소 전파가 분리되어 흐름 추적이 어려워집니다.
어떻게: 같은 코루틴 안에서 결과를 확인하고 loadAnswers() → 상태 업데이트를 순차 실행하도록 구조를 단순화하는 것이 좋습니다.

♻️ 제안 리팩터링
-                    answerRepository
-                        .deleteCurrentAnswer(questionId)
-                        .onSuccess {
-                            // loadAnswers()가 완료될 때까지 기다린 후 상태 확인
-                            viewModelScope.launch {
-                                loadAnswers()
-
-                                // loadAnswers() 완료 후 히스토리에서 복원된 답변이 있는지 확인
-                                val hasAnswerAfterDelete = _uiState.value.currentAnswer != null
-                                if (!hasAnswerAfterDelete) {
-                                    // 모든 답변이 삭제되었으면 미해결 상태로 변경
-                                    updateQuestionSolvedStatus(false)
-                                }
-                            }
-                        }
+                    val result = answerRepository.deleteCurrentAnswer(questionId)
+                    if (result.isSuccess) {
+                        loadAnswers()
+                        val hasAnswerAfterDelete = _uiState.value.currentAnswer != null
+                        if (!hasAnswerAfterDelete) {
+                            updateQuestionSolvedStatus(false)
+                        }
+                    }

Comment on lines +50 to +63
private suspend fun loadAnswers() {
val currentResult = answerRepository.fetchCurrentAnswer(questionId)
val currentAnswer = currentResult.getOrNull()

val historyResult = answerRepository.fetchAnswerHistory(questionId)
val historyAnswers = historyResult.getOrElse { emptyList() }

val currentAnswerUi: AnswerUiModel.Current? =
currentAnswer?.toUiModel() as? AnswerUiModel.Current

val historyAnswersUi: List<AnswerUiModel.History> =
historyAnswers.mapNotNull { it.toUiModel() as? AnswerUiModel.History }

_uiState.update { it.updateAnswers(currentAnswerUi, historyAnswersUi) }
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, let's find and read the QuestionDetailViewModel file
find . -name "QuestionDetailViewModel.kt" -type f

Repository: chanho0908/DroidMorning

Length of output: 170


🏁 Script executed:

# Also check the file size before reading
wc -l composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/vm/QuestionDetailViewModel.kt

Repository: chanho0908/DroidMorning

Length of output: 172


🏁 Script executed:

# Let's look at the UiState definition
find . -name "*QuestionDetail*" -type f | grep -E "\.(kt|java)$"

Repository: chanho0908/DroidMorning

Length of output: 713


🏁 Script executed:

# Read the full QuestionDetailViewModel
cat -n composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/vm/QuestionDetailViewModel.kt

Repository: chanho0908/DroidMorning

Length of output: 6958


🏁 Script executed:

# Read the UiState model
cat -n composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/model/QuestionDetailUiState.kt

Repository: chanho0908/DroidMorning

Length of output: 2693


🏁 Script executed:

# Check the answer repository to understand Result type
find . -name "*AnswerRepository*" -type f | head -5

Repository: chanho0908/DroidMorning

Length of output: 251


🏁 Script executed:

# Check the AnswerRepository to understand error handling pattern
cat -n ./data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultAnswerRepository.kt | head -80

Repository: chanho0908/DroidMorning

Length of output: 1848


🏁 Script executed:

# Check the screen to see if there's error handling there
cat -n composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/QuestionDetailScreen.kt | head -100

Repository: chanho0908/DroidMorning

Length of output: 5242


네트워크 실패가 조용히 무시되어 UX가 모호해질 수 있어요.

왜: getOrNull()getOrElse { emptyList() }로 실패를 삼키면, 실제 오류가 발생해도 화면에는 "데이터 없음"처럼 보입니다. 특히 QuestionDetailUiState에 에러 상태가 없어서, 사용자는 네트워크 오류인지 데이터가 진짜 없는 건지 구분할 수 없어요.

어떻게: 같은 파일의 onToggleFavorite() (140~144줄)처럼 onFailure 핸들러를 추가하거나, UiState에 에러 상태를 추가해 스낵바 등으로 실패를 알리는 것이 좋습니다. 실패 시 어떤 UX를 의도하셨나요? (에러 표시 vs 기존 데이터 유지 등)

🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/vm/QuestionDetailViewModel.kt`
around lines 50 - 63, The loadAnswers() call swallows network failures via
getOrNull() and getOrElse { emptyList() }, making UI indistinguishable between
"no data" and "error"; modify loadAnswers() to handle failures explicitly by
using Result.onFailure/onSuccess (or check Result.isFailure) for
answerRepository.fetchCurrentAnswer(questionId) and
fetchAnswerHistory(questionId), and update the _uiState (same state updated by
_uiState.update) to carry an error flag/message (or trigger the same
snackbar/error path used by onToggleFavorite()) on failure so the UI can surface
network errors instead of silently showing empty data.

Comment on lines +6 to +10
@Serializable
data class UpdateAnswerRequest(
@SerialName("content")
val content: String,
)
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
# Check CreateAnswerRequest definition and usage
echo "=== CreateAnswerRequest 정의 ==="
fd -e kt CreateAnswerRequest | head -5 | xargs cat

echo ""
echo "=== CreateAnswerRequest 사용처 ==="
rg -n "CreateAnswerRequest" --type=kt -B 2 -A 5 | head -50

echo ""
echo "=== UpdateAnswerRequest 정의 확인 ==="
fd -e kt UpdateAnswerRequest | head -5 | xargs cat

echo ""
echo "=== UpdateAnswerRequest 사용처 ==="
rg -n "UpdateAnswerRequest" --type=kt -B 2 -A 5 | head -50

echo ""
echo "=== 다른 Request 클래스 패턴 확인 ==="
fd -e kt ".*Request.kt" data/ | head -10 | xargs cat

Repository: chanho0908/DroidMorning

Length of output: 2031


API 명명 규칙의 일관성 확인이 필요합니다.

답변 생성(CreateAnswerRequest)과 답변 수정(UpdateAnswerRequest)에서 @SerialName 규칙이 다릅니다:

  • CreateAnswerRequest: @SerialName("p_content") (p_ 접두사 사용)
  • UpdateAnswerRequest: @SerialName("content") (접두사 없음)

프로젝트의 다른 요청 클래스들도 이 패턴이 섞여 있습니다 (LikeRequest는 접두사 없음, RpcDefaultRequest는 p_ 사용).

확인 필요사항:

  • 두 클래스가 동일한 백엔드 API를 호출한다면, 일관된 명명 규칙을 적용해야 합니다
  • 서로 다른 API 엔드포인트나 저장 프로시저를 사용한다면, 그 이유를 문서화하는 것이 좋습니다

동일한 리소스(답변)를 다루는 작업이므로, 같은 규칙을 따르도록 정렬하거나 그 차이가 의도된 설계라면 주석으로 명시하는 것을 권장합니다.

🤖 Prompt for AI Agents
In
`@data/src/commonMain/kotlin/com/peto/droidmorning/data/model/request/UpdateAnswerRequest.kt`
around lines 6 - 10, UpdateAnswerRequest uses `@SerialName`("content") while
CreateAnswerRequest and some RPC requests use the "p_" prefix (e.g.,
`@SerialName`("p_content")), causing inconsistent API field names; confirm whether
UpdateAnswerRequest targets the same backend endpoint as CreateAnswerRequest and
if so update UpdateAnswerRequest to use `@SerialName`("p_content") to match
CreateAnswerRequest (or vice versa if the agreed convention is no prefix),
otherwise add a clear comment above the UpdateAnswerRequest data class
documenting why it intentionally differs (reference the UpdateAnswerRequest and
CreateAnswerRequest classes and related LikeRequest / RpcDefaultRequest patterns
when making the change).

import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
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:

# First, check the actual ConfirmDialog.kt file to see the full context
cat -n designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/ConfirmDialog.kt

Repository: chanho0908/DroidMorning

Length of output: 6414


🏁 Script executed:

# Check the project structure and build files
fd -e gradle -e kts | head -20

Repository: chanho0908/DroidMorning

Length of output: 207


🏁 Script executed:

# Look for Compose Multiplatform configuration in build files
fd -e gradle -e kts | xargs grep -l "compose\|jetbrains" 2>/dev/null | head -10

Repository: chanho0908/DroidMorning

Length of output: 207


🏁 Script executed:

# Search for all Preview imports in the codebase
rg -n "import.*[Pp]review\.[Pp]review" --type kotlin -i

Repository: chanho0908/DroidMorning

Length of output: 6609


🏁 Script executed:

# Check the designsystem build.gradle.kts for Compose configuration
cat -n designsystem/build.gradle.kts | head -50

Repository: chanho0908/DroidMorning

Length of output: 1524


🏁 Script executed:

# Check the root build.gradle.kts for Compose Multiplatform setup
cat -n build.gradle.kts | head -80

Repository: chanho0908/DroidMorning

Length of output: 1097


🏁 Script executed:

# Check if there are any androidMain source set usages for preview
rg -n "androidMain.*kotlin" --type gradle --type kts

Repository: chanho0908/DroidMorning

Length of output: 94


🏁 Script executed:

# Check directory structure for designsystem source sets
fd -type d "src/(commonMain|androidMain|iosMain)" designsystem/

Repository: chanho0908/DroidMorning

Length of output: 237


🏁 Script executed:

# Check gradle/libs.versions.toml for Compose Multiplatform version
fd -e toml | xargs grep -A2 -B2 "compose"

Repository: chanho0908/DroidMorning

Length of output: 3510


🏁 Script executed:

# Check if jetbrains compose preview is available in the versions
grep -r "jetbrains.compose" . --include="*.toml" --include="*.gradle" --include="*.kts" 2>/dev/null | head -20

Repository: chanho0908/DroidMorning

Length of output: 1380


Compose Multiplatform에 맞는 Preview import로 변경하세요

androidx.compose.ui.tooling.preview.Preview는 Android 전용 API입니다. 현재 파일이 commonMain 소스셋에 위치해 있어 iOS/Desktop 등 다른 플랫폼을 위해 컴파일할 때 이 import를 찾을 수 없어 빌드가 실패합니다.

해결 방법:
프로젝트가 이미 Compose Multiplatform의 tooling-preview 라이브러리를 의존성에 포함하고 있으므로, 다음과 같이 변경하면 됩니다:

// 변경 전
import androidx.compose.ui.tooling.preview.Preview

// 변경 후
import org.jetbrains.compose.ui.tooling.preview.Preview

이렇게 하면 모든 플랫폼(Android, iOS, Desktop)에서 Preview 기능을 일관되게 사용할 수 있습니다.

🤖 Prompt for AI Agents
In
`@designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/ConfirmDialog.kt`
at line 27, The imported Preview is Android-only: replace the Android-specific
import androidx.compose.ui.tooling.preview.Preview with the Compose
Multiplatform tooling-preview import
org.jetbrains.compose.ui.tooling.preview.Preview in ConfirmDialog.kt (update the
import statement that references Preview so the file in commonMain compiles for
iOS/Desktop/Android).

- 중복 문자열 리소스 제거 (last_modified, last_modified_format, date_suffix_*)
- answer_history_count 문자열 리소스에 플레이스홀더 추가
- kotlinx.datetime.Instant를 kotlin.time.Instant로 변경
- IconButton 터치 타겟 크기를 Material Design 가이드라인(48dp)에 맞게 개선
- AnswerHistory 중복 카운트 표시 제거

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
- AnswerHistory에 key 추가하여 아이템 상태 혼동 방지
- QuestionDetailViewModel에 onFailure 핸들러 추가 및 질문 미발견 시 로딩 상태 해제
- CurrentAnswerResponse에서 kotlinx.datetime.Instant를 kotlin.time.Instant로 변경
- domain 모듈에서 사용하지 않는 kotlinx.datetime 의존성 제거

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <[email protected]>
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: 1

🤖 Fix all issues with AI agents
In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/AnswerCard.kt`:
- Around line 20-21: The commonMain source imports use Android-only preview
symbols causing iOS build failures; update the imports of Preview and
PreviewParameter in AnswerCard.kt (and other commonMain files) from
androidx.compose.ui.tooling.preview.Preview and
androidx.compose.ui.tooling.preview.PreviewParameter to the Compose
Multiplatform variants org.jetbrains.compose.ui.tooling.preview.Preview and
org.jetbrains.compose.ui.tooling.preview.PreviewParameter so the Preview and
PreviewParameter symbols are platform-independent.

Comment on lines +20 to +21
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
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:

# Check all Preview imports in commonMain to see what pattern is used
rg -n "import.*Preview" composeApp/src/commonMain/kotlin --type=kt

Repository: chanho0908/DroidMorning

Length of output: 93


🏁 Script executed:

# Check if there are any iOS or other platform-specific main directories
fd -t d "Main" composeApp/src --type d

Repository: chanho0908/DroidMorning

Length of output: 145


🏁 Script executed:

# Look at the project build configuration to understand target platforms
find . -name "build.gradle.kts" -o -name "build.gradle" | head -5

Repository: chanho0908/DroidMorning

Length of output: 197


🏁 Script executed:

# Check Preview imports in commonMain without type flag
rg "import.*Preview" composeApp/src/commonMain

Repository: chanho0908/DroidMorning

Length of output: 4243


🏁 Script executed:

# Check composeApp build configuration to see target platforms
head -60 composeApp/build.gradle.kts

Repository: chanho0908/DroidMorning

Length of output: 1745


🏁 Script executed:

# Check if androidMain has different Preview imports
rg "import.*Preview" composeApp/src/androidMain

Repository: chanho0908/DroidMorning

Length of output: 49


🏁 Script executed:

# Check commonMain dependencies in build.gradle.kts
grep -A 100 "commonMain.dependencies" composeApp/build.gradle.kts

Repository: chanho0908/DroidMorning

Length of output: 1904


🏁 Script executed:

# Check if there's a dependsOn or expect/actual pattern for Preview
rg "expect.*Preview|actual.*Preview" composeApp/src

Repository: chanho0908/DroidMorning

Length of output: 49


🏁 Script executed:

# Check the libs.versions.toml or similar for compose dependency versions
fd -e "versions.toml" -o -e "libs.versions.toml" . | xargs cat

Repository: chanho0908/DroidMorning

Length of output: 183


🏁 Script executed:

# Check if there's a libs.versions.toml to understand compose.multiplatform bundle
find . -name "*.toml" | head -5 | xargs head -100

Repository: chanho0908/DroidMorning

Length of output: 153


Compose Multiplatform 크로스 플랫폼 호환성을 위해 Preview 임포트 수정이 필요합니다.

현재 AnswerCard.kt를 포함한 프로젝트의 commonMain 파일들에서 androidx.compose.ui.tooling.preview.Preview를 사용하고 있습니다. 프로젝트가 Android와 iOS 모두를 타겟팅하고 있기 때문에, commonMain의 코드는 플랫폼 독립적이어야 합니다.

왜 수정이 필요한가요?

  • androidx.compose 패키지는 Android 전용 라이브러리입니다
  • iOS 빌드 시 이 임포트들을 인식할 수 없어 컴파일 오류가 발생할 수 있습니다
  • Compose Multiplatform에서는 org.jetbrains.compose.ui.tooling.preview.Preview를 사용해야 합니다

해결 방법:
Preview 및 PreviewParameter 임포트를 다음과 같이 변경하세요:

import org.jetbrains.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.ui.tooling.preview.PreviewParameter

이 패턴은 프로젝트의 다른 commonMain 파일들에도 동일하게 적용되므로, 일괄 수정을 권장합니다.

🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/questions/detail/component/AnswerCard.kt`
around lines 20 - 21, The commonMain source imports use Android-only preview
symbols causing iOS build failures; update the imports of Preview and
PreviewParameter in AnswerCard.kt (and other commonMain files) from
androidx.compose.ui.tooling.preview.Preview and
androidx.compose.ui.tooling.preview.PreviewParameter to the Compose
Multiplatform variants org.jetbrains.compose.ui.tooling.preview.Preview and
org.jetbrains.compose.ui.tooling.preview.PreviewParameter so the Preview and
PreviewParameter symbols are platform-independent.

@chanho0908 chanho0908 self-assigned this Jan 17, 2026
@chanho0908 chanho0908 merged commit 0ae09ba into main Jan 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

문제 상세 화면(QuestionDetailScreen) 구현

1 participant