Skip to content

Comments

인증샷 수정 화면 UI 구현 및 API 연동#87

Open
chanho0908 wants to merge 60 commits intodevelopfrom
feat/#85-task-certification-update
Open

인증샷 수정 화면 UI 구현 및 API 연동#87
chanho0908 wants to merge 60 commits intodevelopfrom
feat/#85-task-certification-update

Conversation

@chanho0908
Copy link
Member

@chanho0908 chanho0908 commented Feb 14, 2026

이슈 번호

리뷰/머지 희망 기한 (선택)

작업내용

  • 인증샷 수정 기능
  • 키보드가 올라올시 Comment 컴포저블을 키보드 위로 올리는 컴포저블 분리
  • 인증샷 이미지를 렌더링 하는 컴포저블을 재활용하기 위해 디자인 시스템으로 이동

결과물

🛠 코멘트 수정 / 인증샷 수정

코멘트 수정

default.mp4

인증샷 수정

default.mp4

리뷰어에게 추가로 요구하는 사항 (선택)

- 인증샷 편집 화면 및 ViewModel을 추가했습니다.
- 촬영된 인증샷 이미지와 코멘트를 표시합니다.
- '다시찍기' 버튼을 통해 카메라 화면으로 이동할 수 있습니다.
- DI 설정에 `TaskCertificationEditorViewModel`을 추가했습니다.
# Conflicts:
#	feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetail.kt
@chanho0908 chanho0908 self-assigned this Feb 14, 2026
@chanho0908 chanho0908 added the Feature Extra attention is needed label Feb 14, 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: 5

🤖 Fix all issues with AI agents
In `@feature/task-certification/build.gradle.kts`:
- Line 13: The module only adds implementation(libs.kotlinx.serialization.json)
but doesn't apply the Kotlin serialization plugin; fix by either updating
FeatureConventionPlugin to apply<SerializationConventionPlugin>() (or apply the
plugin in the same style as DataConventionPlugin within FeatureConventionPlugin)
so the org.jetbrains.kotlin.plugin.serialization is available at compile time,
or add a plugins block in this module's build.gradle.kts that declares
alias(libs.plugins.kotlinx.serialization); ensure the `@Serializable-using`
classes are compiled with the serialization plugin enabled.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt`:
- Around line 143-147: The when branch handling
NavRoutes.TaskCertificationRoute.From currently does nothing for EDITOR; update
the branch in TaskCertificationViewModel so that
NavRoutes.TaskCertificationRoute.From.EDITOR calls
taskCertificationRefreshBus.notifyChanged() (same as the DETAIL branch) so
TaskCertificationDetailViewModel (which subscribes to
taskCertificationRefreshBus.events and invokes fetchPhotolog()) will receive the
refresh signal after an upload from the editor; leave the HOME branch invoking
goalRefreshBus.notifyGoalListChanged() unchanged.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorRoute.kt`:
- Around line 143-179: The UI uses hardcoded Spacer heights (Spacer(103.dp) and
Spacer(101.dp)) around PhotologCard which can overflow on small screens; replace
these fixed gaps by either making the parent Column scrollable (e.g., add
verticalScroll) or use flexible layout like Modifier.weight on surrounding
containers to distribute space (adjust PhotologCard, Spacer, and AppRoundButton
placement accordingly), and if you must keep fixed values extract them as named
constants (e.g., PHOTolog_TOP_GAP, PHOTO_BOTTOM_GAP) with comments to clarify
intent; update references to PhotologCard, AppRoundButton, and the
onGloballyPositioned logic so the layout adapts without clipping.
- Around line 57-114: Implement the Save flow: in the ViewModel's handleIntent()
replace the TODO in the TaskCertificationEditorIntent.Save case to call
PhotoLogRepository (inject it into the ViewModel), perform the API call in a
coroutine, and emit appropriate TaskCertificationEditorSideEffect values for
success and failure (e.g., NavigateBack on success and ShowError(message) on
failure); add those SideEffect sealed subclasses to
TaskCertificationEditorSideEffect. Then update TaskCertificationEditorRoute to
collect viewModel.sideEffects (or uiState.sideEffect flow) and handle them by
calling navigateToBack on NavigateBack and using toastManager.show(...) for
ShowError, keeping existing permission and navigation logic intact so Save
triggers the network call and UI reacts to the emitted side effects.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorViewModel.kt`:
- Line 42: The current handler for TaskCertificationEditorIntent.Save uses
TODO("인증샷 수정 API 연동") which throws NotImplementedError at runtime; replace that
TODO with a safe temporary implementation in the
TaskCertificationEditorViewModel: handle TaskCertificationEditorIntent.Save by
logging the action and emitting a UI event or state (e.g., showToast /
SaveNotAvailable / SaveFailed) instead of throwing, so pressing Save won't crash
until the real API integration is implemented; update references around
TaskCertificationEditorIntent.Save and any existing event/state emission helpers
in the ViewModel to surface the toast/log to the UI.
🧹 Nitpick comments (20)
feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt (1)

81-92: actionTitle이 null일 때 불필요한 클릭 핸들러가 설정됩니다

현재 actionTitlenull이고 onClickModifynull인 경우에도 .noRippleClickable이 항상 적용됩니다. 기능적으로 문제는 없지만, 접근성(Accessibility) 관점에서 빈 박스가 클릭 가능 요소로 인식될 수 있습니다.

onClickModify가 null이 아닐 때만 클릭 핸들러를 연결하면 의도가 더 명확해질 수 있는데, 어떻게 생각하시나요?

♻️ 개선 제안
             Box(
                 modifier =
                     Modifier
                         .width(60.dp)
                         .fillMaxHeight()
                         .background(
                             if (actionTitle == null) {
                                 CommonColor.White
                             } else {
                                 GrayColor.C100
                             },
-                        ).noRippleClickable { onClickModify?.invoke() },
+                        ).then(
+                            if (onClickModify != null) {
+                                Modifier.noRippleClickable(onClick = onClickModify)
+                            } else {
+                                Modifier
+                            }
+                        ),
                 contentAlignment = Alignment.Center,
             ) {
core/design-system/src/main/java/com/twix/designsystem/components/button/AppRoundButton.kt (1)

28-29: 다크 모드 지원 검토가 필요합니다

현재 textColorbackgroundColor를 호출부에서 직접 하드코딩된 색상(GrayColor.C500, CommonColor.White)으로 전달하고 있습니다. 이 컴포저블 자체는 Color 파라미터를 받으므로 구조적으로는 문제가 없지만, 다크 모드에서 적절한 색상이 전달되는지는 호출부의 책임이 됩니다.

디자인 시스템 컴포넌트로서 다크 모드 안전성을 높이려면, 기본값을 테마 기반 색상으로 제공하는 방안을 고려해볼 수 있습니다. 예를 들어:

textColor: Color = TwixTheme.colors.onSurface,
backgroundColor: Color = TwixTheme.colors.surface,

현재 필수 파라미터로 두는 설계도 유효하지만, 프리뷰에 다크 모드 프리뷰를 추가하면 시각적 검증에 도움이 됩니다.

As per coding guidelines, "Dark Mode 지원 여부" 확인이 필요합니다.

core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt (2)

95-106: 숨겨진 TextField의 접근성(Accessibility) 고려가 필요합니다

width(0.dp)alpha(0f)로 숨겨진 TextField는 TalkBack 등 스크린 리더에서 여전히 포커스를 받을 수 있어, 시각 장애 사용자에게 혼란을 줄 수 있습니다.

직접 변경된 코드는 아니지만, 접근성 개선 시 semantics { invisibleToUser() } 또는 커스텀 contentDescription을 통해 스크린 리더가 이 컴포넌트의 역할을 올바르게 안내하도록 개선하는 것을 고려해보시면 좋겠습니다.

As per coding guidelines, core/design-system/**: 접근성(Accessibility) 고려 여부.


119-124: Material Design 3 및 다크 모드 지원을 위해 색상 토큰 도입이 필요합니다

현재 GrayColor.C500(#171717 - 매우 어두운 색상)이 하드코딩되어 있어 다크 모드에서 배경과의 명도 대비가 부족합니다. 이는 접근성 측면에서도 문제가 될 수 있습니다.

설계 시스템이 Material Design 3를 준수하려면, 현재의 정적 색상 객체 대신 MaterialTheme.colorScheme의 동적 토큰(예: outline, outlineVariant)을 사용하여 라이트/다크 모드를 자동으로 지원해야 합니다.

개선 방향:

  • 테마 시스템을 Material Design 3 ColorScheme 기반으로 구축
  • 고정 색상 대신 isSystemInDarkTheme() 기반의 조건부 색상 선택 또는 ColorScheme 토큰 사용
  • 현재 디자인 시스템 전체에서 이 패턴이 반복되므로, 아키텍처 차원의 개선을 검토하면 좋겠습니다
core/navigation/build.gradle.kts (1)

5-5: Serialization 플러그인 및 의존성 추가가 적절합니다.

Navigation 모듈에서 TaskCertificationSerializer를 통해 경로 데이터를 직렬화하기 위한 설정으로, implementation 스코프 사용도 적절합니다.

다만, 프로젝트에 serialization을 사용하는 모듈이 늘어날 경우, twix.koin처럼 serialization 전용 Convention Plugin을 만드는 것도 고려해볼 수 있습니다. 현재 feature/task-certification/build.gradle.kts에서도 동일한 의존성을 추가하고 있으므로 중복 설정을 줄일 수 있습니다.

Also applies to: 12-14

core/design-system/src/main/java/com/twix/designsystem/components/photolog/PhotologCard.kt (1)

24-47: 재사용성을 높이기 위한 public 전환과 기본값 설정이 잘 되어 있습니다.

modifier 파라미터를 외부에서 주입 가능하게 하고, 색상에 기본값을 제공한 점이 좋습니다.

한 가지 고려할 점은, CommonColor.WhiteGrayColor.C500 같은 하드코딩된 색상이 다크 모드에서 적절하지 않을 수 있다는 것입니다. 디자인 시스템 가이드라인에 따르면 다크 모드 지원 여부를 확인해야 합니다. 현재 앱이 다크 모드를 지원하지 않는다면 괜찮지만, 향후 지원 계획이 있다면 MaterialTheme.colorScheme 기반의 테마 색상 사용을 검토해보시는 것이 좋겠습니다.

feature/task-certification/src/main/java/com/twix/task_certification/certification/component/TaskCertificationTopBar.kt (1)

32-39: 닫기 버튼의 contentDescriptionnull입니다 — 접근성 개선이 필요합니다.

스크린 리더 사용자가 닫기 버튼의 용도를 인식할 수 없습니다. 디자인 시스템 리뷰 가이드에서 접근성(Accessibility) 고려를 요구하고 있으므로, contentDescription에 의미 있는 문자열을 제공하는 것이 좋겠습니다.

♿ 접근성 개선 제안
         Image(
             imageVector = ImageVector.vectorResource(R.drawable.ic_close_c100),
-            contentDescription = null,
+            contentDescription = stringResource(R.string.word_cancel),

As per coding guidelines, 디자인 시스템 리뷰 가이드: "접근성(Accessibility) 고려 여부".

core/design-system/src/main/java/com/twix/designsystem/components/photolog/CertificatedCard.kt (2)

20-48: Public 디자인 시스템 컴포넌트에 modifier 파라미터가 없습니다.

PhotologCardmodifier 파라미터를 노출하고 있는 반면, CertificatedCard는 그렇지 않습니다. Public 컴포저블에서 modifier를 노출하는 것은 Compose의 API 가이드라인에서 권장하는 패턴입니다. 호출부에서 패딩, 크기 등을 조정할 수 없게 됩니다.

♻️ modifier 파라미터 추가 제안
 `@Composable`
 fun CertificatedCard(
     imageUrl: String?,
     comment: String?,
+    modifier: Modifier = Modifier,
 ) {
-    Box(Modifier.fillMaxSize()) {
+    Box(modifier.fillMaxSize()) {

33-34: 인증샷 이미지의 contentDescriptionnull입니다.

디자인 시스템 가이드라인의 접근성 항목에 따라, 인증샷 이미지에 적절한 콘텐츠 설명을 제공하는 것이 좋겠습니다.

As per coding guidelines, 디자인 시스템 리뷰 가이드: "접근성(Accessibility) 고려 여부".

core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt (1)

70-77: 찌르기 아이콘의 contentDescriptionnull입니다.

스크린 리더 사용자를 위해 의미 있는 설명을 추가하면 좋겠습니다.

♿ 접근성 개선 제안
             Image(
                 imageVector = ImageVector.vectorResource(R.drawable.ic_keepi_sting),
-                contentDescription = null,
+                contentDescription = stringResource(R.string.partner_sting),

As per coding guidelines, 디자인 시스템 리뷰 가이드: "접근성(Accessibility) 고려 여부".

core/navigation/src/main/java/com/twix/navigation/serializer/TaskCertificationSerializer.kt (1)

5-12: 네이밍에 대해 한 가지 제안드립니다.

TaskCertificationSerializer라는 이름이 kotlinx.serialization의 KSerializer<T>와 혼동될 수 있습니다. 이 클래스는 실제로 네비게이션 간 데이터를 전달하는 DTO(Data Transfer Object) 역할을 하므로, TaskCertificationEditorArgs 또는 TaskCertificationNavArgs 같은 이름이 역할을 더 명확히 전달할 수 있습니다.

기존 프로젝트 컨벤션이 *Serializer라면 유지해도 무방합니다.

feature/task-certification/src/main/java/com/twix/task_certification/editor/model/TaskCertificationEditorUiState.kt (1)

9-16: State 정의가 MVI 패턴에 잘 부합합니다.

@Immutable 어노테이션과 copy()를 활용한 불변 상태 관리가 올바릅니다. 두 가지 의견을 드립니다:

  1. isImageChanged: 현재 어떤 mutation helper에서도 업데이트되지 않습니다. "다시 찍기" 플로우에서 사용될 예정이라면 괜찮지만, 향후 구현 계획이 없다면 YAGNI 원칙에 따라 제거를 고려해 주세요.

  2. photologId = -1: 센티널 값(-1) 대신 Long?(nullable)을 사용하면 초기화되지 않은 상태를 타입 시스템으로 보호할 수 있습니다. 다만 기존 프로젝트 컨벤션이 -1을 사용한다면 일관성을 위해 유지해도 됩니다.

core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentAnchorFrame.kt (2)

48-48: anchorBottom == 0f early return에 대한 엣지 케이스 고려

현재 0f를 "아직 측정되지 않은 상태"로 사용하고 있는데, 이론적으로 앵커 요소가 화면 최상단에 위치하여 bottom이 실제로 0f인 경우도 early return되어 CommentBox가 렌더링되지 않습니다. 실제 사용 맥락에서는 TopBar 등이 있어 0f가 될 가능성은 극히 낮지만, 명시적으로 null 가능 타입(Float?)을 사용하면 "미측정"과 "실제 0" 상태를 구분할 수 있습니다.

현재 구조에서 실질적 문제가 될 가능성은 낮으므로 참고만 해주세요.


40-104: 디자인 시스템 컴포넌트에 Preview가 빠져 있습니다.

코딩 가이드라인에 따르면 Composable 함수에 Preview가 제공되어야 합니다. CommentAnchorFrameanchorBottomCommentUiModel 등의 파라미터가 필요하므로, 다양한 상태(포커스 on/off, 키보드 활성화 등)에 대한 Preview를 추가하면 디자인 검증에 도움이 됩니다.

As per coding guidelines: core/design-system/** — Compose UI 가이드라인에서 "Preview Composable이 제공되는가?" 항목에 해당합니다.

feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorRoute.kt (2)

136-141: TopBar의 onClickModify 파라미터에 onClickSave를 전달하는 부분이 혼란스러울 수 있습니다.

TaskCertificationDetailTopBar는 상세 화면용으로 설계되어 onClickModify라는 파라미터명을 사용하지만, 편집 화면에서는 "저장" 기능으로 사용되고 있습니다. 현재 기능적으로는 문제없지만, 향후 유지보수를 위해 TopBar의 파라미터명을 onClickAction처럼 범용적으로 변경하거나, 편집 화면 전용 TopBar를 만드는 것을 고려해 볼 수 있습니다.


169-179: AppRoundButtononClick 파라미터를 지원하도록 API를 개선해볼 수 있습니다.

현재 코드는 AppRoundButtononClick 파라미터를 제공하지 않아 외부 Modifier로 클릭 핸들러를 분리하고 있습니다. 기능적으로는 정상 작동하며, noRippleClickable은 내부적으로 Modifier.clickable()을 사용하므로 접근성 시맨틱 정보(버튼 역할, 클릭 액션)도 자동으로 제공됩니다.

다만 컴포넌트 사용 입장에서 보면, 버튼이 클릭 핸들러를 직접 파라미터로 받을 수 있다면 더 직관적이고 명확한 API가 될 것입니다. 예를 들어:

fun AppRoundButton(
    text: String,
    textColor: Color,
    backgroundColor: Color,
    onClick: () -> Unit,  // 추가
    modifier: Modifier = Modifier,
    // ... 기타 파라미터
)

이렇게 개선하면 사용처에서 modifiernoRippleClickable을 명시적으로 추가할 필요 없이, 더 간결한 코드를 작성할 수 있습니다.

core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt (1)

68-77: Json 인스턴스 일관성 확인 필요

Line 73에서 Json.encodeToString(data)는 기본 Json 설정을 사용합니다. TaskCertificationEditorViewModel의 디코딩 측(Line 26)에서도 기본 Json을 사용하고 있어 현재는 동작하지만, 프로젝트 내 다른 곳에서 커스텀 Json 설정(예: ignoreUnknownKeys = true)을 사용한다면 인코딩/디코딩 불일치가 발생할 수 있습니다.

공통 Json 인스턴스를 DI나 싱글톤으로 관리하는 것을 고려해 보시면 향후 직렬화 설정 변경 시 일관성을 유지하기 쉬워집니다.

feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorViewModel.kt (1)

20-28: serializer 프로퍼티의 가시성과 에러 처리

두 가지 포인트를 남깁니다:

  1. serializerpublic val로 노출되어 있는데, ViewModel 내부 초기화(reduceInitialState)에서만 사용된다면 private으로 제한하는 것이 캡슐화에 더 적합합니다. 외부에서 접근이 필요한 경우라면 현재 그대로 유지해도 됩니다.

  2. requireNotNull은 데이터가 없을 때 IllegalArgumentException으로 크래시합니다. 딥링크나 프로세스 복원 등 예외적 상황에서 SavedStateHandle에 데이터가 없을 수 있는데, 이 경우 사용자에게 에러 화면을 보여주거나 뒤로 이동하는 것이 더 나은 UX일 수 있습니다. 현재 Navigation 흐름에서 항상 보장된다면 괜찮지만, 방어적 처리를 고려해 보시는 것도 좋겠습니다.

feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailUiState.kt (1)

65-72: toSerializer()에서 myPhotolog가 null인 경우의 방어 처리가 잘 되어 있습니다

canModifytrue일 때만 수정 버튼이 노출되므로, 실제로 myPhotolog가 null인 상태에서 이 함수가 호출될 가능성은 낮습니다. 다만 photologId = -1이 서버 API에 그대로 전달되면 의도치 않은 결과가 발생할 수 있으니, 에디터 ViewModel의 Save 로직 구현 시 유효성 검증을 추가하시면 좋겠습니다.

feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetail.kt (1)

63-63: navigateToEditorTaskCertificationDetailUiState를 전달하는 설계에 대해

navigateToEditor의 파라미터로 전체 UiState를 전달하고, navigation 레이어에서 toSerializer()를 호출하는 구조입니다. 이 방식은 동작상 문제는 없지만, Navigation 콜백이 UI 상태 전체를 알아야 한다는 점에서 결합도가 다소 높습니다.

대안으로 navigateToEditor: (TaskCertificationSerializer) -> Unit으로 시그니처를 변경하고, 호출부에서 navigateToEditor(uiState.toSerializer())로 변환하면 navigation 레이어가 UiState 타입에 의존하지 않게 됩니다. 다만 현재 구조에서도 충분히 동작하므로, 리팩터링 우선순위에 따라 판단하시면 됩니다.

@chanho0908 chanho0908 requested a review from dogmania February 14, 2026 13:09
@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 15, 2026
@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 15, 2026
…fication-update

# Conflicts:
#	data/src/main/java/com/twix/data/repository/DefaultPhotoLogRepository.kt
#	feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetail.kt
#	feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetailViewModel.kt
#	feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailUiState.kt
#	feature/task-certification/src/main/res/values/strings.xml
@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 18, 2026
Copy link
Member

@dogmania dogmania left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! 네비게이션 파라미터로 데이터 넘기는 방식은 장기적으로 봤을 때 id 기반으로 Room, 서버에서 데이터를 가져오는 식으로 수정하는 게 확장하거나 수정에 더 안정적일 거 같아요 지금 당장 수정할지 말지는 작업이 아직 남아있어서 같이 고민해봐요

Copy link
Member

Choose a reason for hiding this comment

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

네비게이션 전용으로 사용되는 거면 NavArgs로 네이밍을 짓는 건 어떨까요 패키지명도 args로 잡으면 좋을 거 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

오 ! 좋은 아이디어네 👍

리뷰 반영 커밋 : 06c4d97

) = "task_certification/$goalId/${from.name}"
fun createRoute(data: DetailSerializer): String {
val json = Json.encodeToString(data)
val encoded = Uri.encode(json)
Copy link
Member

Choose a reason for hiding this comment

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

네비게이션 전용으로 이렇게 인코딩해서 넣는 게 지금 당장에는 문제가 없을 거 같긴 하지만 나중에 데이터에 변경이 생기는 경우를 고려하면 좋을 거 같아요. 예를 들어서 앱 버전이 올라가면서 nullable이 추가되거나, 있던 변수가 사라지거나, 새로 추가되는 변수가 있거나 이러면 serialization 과정에서 앱이 죽어버릴 수도 있을 거 같아요

id랑 필요하면 날짜 정도만 넣어서 넘기는 걸로 하고 뷰모델에서 새로 데이터를 조회하거나 아니면 뷰모델을 공유하는 방식이 안정성 측면에서는 더 좋을 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

나도 동의해 ! 현수가 말한 데이터들이나 반드시 필요한 값들을 전달하면 좋을 것 같아 !
ex) 코멘트 수정 화면 -> 인증샷 수정 화면 이동시 코멘트 수정 화면에서 수정한 코멘트 전달

요긴 인증샷 단일 조회 API가 생기면 변경점이 좀 생길 것 같아서 단일 조회 API로 수정되면 다시 이야기 해보자 🙂

Copy link
Member

Choose a reason for hiding this comment

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

형이 전체적으로 수정한다고 하셨는데 리마인드 차원에서 여기 업데이트 로직도 뷰모델로 옮겨주세요!

Copy link
Member Author

Choose a reason for hiding this comment

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

리뷰 반영 커밋 : 47aa09e

Comment on lines 150 to 155
TaskCertificationDetailTopBar(
actionTitle = stringResource(DesR.string.word_save),
goalTitle = uiState.goalName,
onBack = onBack,
onClickModify = onClickSave,
)
Copy link
Member

Choose a reason for hiding this comment

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

CommonTopBar로 수정할 수 있을 거 같아요

Comment on lines 27 to 35
private val serializer =
requireNotNull(
savedStateHandle
.get<String>(NavRoutes.TaskCertificationEditorRoute.ARG_DATA)
?.let { encoded ->
val json = Uri.decode(encoded)
Json.decodeFromString<EditorSerializer>(json)
},
) { SERIALIZER_NOT_FOUND }
Copy link
Member

Choose a reason for hiding this comment

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

지금 에러가 발생했을 때 로깅이나 예외처리가 없어서 앱이 그냥 죽어버릴 거 같은데 원인은 파악할 수 있게 try catch를 추가하거나 최소한의 안전 장치가 있으면 좋을 거 같아요

Copy link
Member Author

Choose a reason for hiding this comment

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

디코딩을 위한 확장함수를 추가했어!

리뷰 반영 커밋 : 2fbe065

@chanho0908
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 20, 2026
@chanho0908 chanho0908 requested a review from dogmania February 20, 2026 09:11
@chanho0908 chanho0908 changed the base branch from feat/#84-certification-modify to develop February 20, 2026 09:23
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: 9

Caution

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

⚠️ Outside diff range comments (6)
feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt (3)

173-175: ⚠️ Potential issue | 🟡 Minor

opacity 스프링에 stiffness 누락 → 좌우/상하 애니메이션과 타이밍 불일치

offsetX, offsetY의 복귀 스프링(157~171번 줄)은 dampingRatiostiffness를 모두 spec에서 가져오는데, opacity의 스프링만 dampingRatio만 전달하고 stiffness는 기본값을 사용합니다. 결과적으로 투명도 복귀 속도가 위치 복귀 속도와 달라져 시각적 불일치가 생깁니다.

🛠 제안 수정
-                    launch {
-                        opacity.animateTo(1f, spring(spec.springDamping))
-                    }
+                    launch {
+                        opacity.animateTo(
+                            1f,
+                            spring(
+                                dampingRatio = spec.springDamping,
+                                stiffness = spec.springStiffness,
+                            ),
+                        )
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt`
around lines 173 - 175, The opacity spring is missing the stiffness setting so
its return timing differs from position springs; update the opacity.animateTo
call (where opacity is animated) to use the same spring spec as offsetX/offsetY
by passing both damping and stiffness from spec (e.g., use the same
springDamping and springStiffness values or the same spec object used for
offsetX/offsetY) so opacity’s timing matches the position animations.

54-56: ⚠️ Potential issue | 🟠 Major

🔴 드래그 중 매 프레임마다 불필요한 리컴포지션 발생

rotation 값이 컴포지션 스코프(54~56번 줄)에서 offsetX.value를 직접 읽고 있습니다. AnimatableMutableState로 백킹되기 때문에, 드래그 중 snapTo가 호출될 때마다 SwipeableCard 전체가 리컴포지션됩니다.

offset { } 람다(64번 줄)와 graphicsLayer { } 람다(73번 줄)는 각각 레이아웃/드로우 단계에서만 state를 읽으므로 리컴포지션을 유발하지 않습니다. rotation 계산도 graphicsLayer 람다 내부로 옮겨 드로우 단계에서만 읽히도록 해야 합니다.

공식 문서에 따르면 graphicsLayer 람다 블록 안에서 State를 읽으면 레이어 속성 업데이트만 발생하고, 리컴포지션과 리레이아웃은 트리거되지 않습니다.

⚡ 제안 수정: rotation 계산을 graphicsLayer 람다 내부로 이동
-    /**
-     * 드래그 거리 기반 회전
-     */
-    val rotation =
-        (offsetX.value / spec.rotationFactor)
-            .coerceIn(-spec.maxRotation, spec.maxRotation)
-
     Box(
         modifier =
             modifier
                 ...
                 /**
                  * 회전 + 투명도 적용
                  */
                 .graphicsLayer {
-                    rotationZ = rotation
+                    rotationZ = (offsetX.value / spec.rotationFactor)
+                        .coerceIn(-spec.maxRotation, spec.maxRotation)
                     alpha = opacity.value
                 }

As per coding guidelines: "불필요한 recomposition 가능성은 없는가?" 항목 위반.

Also applies to: 73-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt`
around lines 54 - 56, The current Compose code computes a top-level rotation val
by reading offsetX.value (val rotation = (offsetX.value /
spec.rotationFactor).coerceIn(...)) which causes recomposition on every snapTo;
move that calculation into the graphicsLayer { } lambda so rotation is read
during the draw phase only (use the same formula with offsetX.value /
spec.rotationFactor and coerceIn using spec.maxRotation) and remove the
top-level rotation val; keep offset { } and graphicsLayer { } usage intact so
only layer properties update and the SwipeableCard composable no longer
recomposes on every Animatable change.

77-89: ⚠️ Potential issue | 🟡 Minor

onDragCancel 미처리 → 제스처 취소 시 카드가 중간 위치에 고착

onDragCancel은 다른 제스처가 드래그를 가로채는 등 외부 요인으로 드래그가 취소될 때 호출됩니다. 현재 onDragCancel이 지정되지 않아 기본값(빈 람다)이 사용되므로, 예를 들어 부모 스크롤러가 제스처를 가로채거나 전화 수신 등의 인터럽트가 발생하면 카드가 오프셋 위치에 그대로 남게 됩니다.

🛠 제안 수정: onDragEnd의 복귀 로직과 동일하게 처리
                     detectDragGestures(
                         onDrag = { change, dragAmount ->
                             ...
                         },
+                        onDragCancel = {
+                            coroutineScope.launch {
+                                launch { offsetX.animateTo(0f, spring()) }
+                                launch { offsetY.animateTo(0f, spring()) }
+                                launch { opacity.animateTo(1f, spring()) }
+                            }
+                        },
                         onDragEnd = {
                             ...
                         },
                     )

Also applies to: 93-184

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt`
around lines 77 - 89, The drag-cancel path is unhandled: add an onDragCancel
handler to the detectDragGestures call(s) so cancellations run the same revert
logic as onDragEnd — i.e., launch the coroutineScope and call
offsetX.snapTo(...) and offsetY.snapTo(...) (the same values/animation used in
onDragEnd) so the card returns to its intended position when a gesture is
intercepted; update every detectDragGestures instance that currently omits
onDragCancel to mirror the onDragEnd behavior.
feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraControlBar.kt (1)

85-113: ⚠️ Potential issue | 🟡 Minor

클릭 가능한 Image 컴포저블에 contentDescription = null — 접근성 개선을 권장합니다

ic_gallery, ic_camera_shutter, ic_camera_toggle, ic_camera_retake 버튼은 모두 사용자와 상호작용하는 인터랙티브 요소임에도 불구하고 contentDescription = null로 설정되어 있습니다. TalkBack과 같은 스크린 리더는 이 버튼들을 "버튼"으로 읽어줄 수 없어 시각 장애 사용자에게 어떤 역할인지 전달되지 않습니다.

단순 장식용(decorative) 이미지에만 null을 사용하고, 인터랙티브 요소에는 의미 있는 설명을 제공하는 것이 좋습니다. 문자열 리소스는 이미 design-system으로 이전 중이므로, 함께 추가하는 것을 제안드립니다:

♻️ 개선 예시 (ic_gallery 버튼 기준)
 Image(
     imageVector = ImageVector.vectorResource(R.drawable.ic_gallery),
-    contentDescription = null,
+    contentDescription = stringResource(R.string.cd_gallery),
     modifier = Modifier
         .size(56.dp)
         .noRippleClickable(enabled = enabled, onClick = onClickGallery),
 )

Also applies to: 128-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraControlBar.kt`
around lines 85 - 113, In CameraControlBar, several interactive Image
composables (the gallery, shutter, toggle and retake buttons referenced by the
Image usages and callbacks onClickGallery, onCaptureClick, onToggleCameraClick
and the retake image) set contentDescription = null; update each to provide
meaningful contentDescription strings (use string resources) instead of null so
screen readers announce the button purpose, e.g. add resource keys like
R.string.cd_gallery_button, R.string.cd_capture_button,
R.string.cd_toggle_camera_button, R.string.cd_retake_button and pass them into
the Image composables' contentDescription via stringResource(...) or equivalent.
core/design-system/src/main/java/com/twix/designsystem/components/button/AppRoundButton.kt (1)

25-34: ⚠️ Potential issue | 🟠 Major

디자인 시스템 컴포넌트에 Role.Button 시맨틱과 onClick 파라미터가 없습니다

AppRoundButton은 시각적으로 버튼처럼 동작하지만, 클릭 처리를 modifier를 통해 호출 측에 완전히 위임합니다. 이 구조에서는 디자인 시스템 라이브러리에서 커스텀 컴포넌트를 만들 때 Role 시맨틱 속성을 직접 설정해야 하며, semantics modifier나 clickable modifier를 통해 설정할 수 있습니다.

현재 설계대로면 이 컴포넌트를 사용하는 모든 호출 측이 직접 Role.Button 시맨틱을 설정해야 하므로, 누락되기 쉬운 구조입니다. 디자인 시스템 컴포넌트는 접근성을 내부에서 보장하는 것이 바람직합니다.

권장 개선 방향: onClick 람다를 파라미터로 받아 컴포넌트 내부에서 role = Role.Button과 함께 clickable을 처리하도록 API를 개선하는 것을 고려해 보세요.

♿ API 개선 제안
+import androidx.compose.foundation.clickable
+import androidx.compose.ui.semantics.Role

 `@Composable`
 fun AppRoundButton(
     text: String,
     textColor: Color,
     backgroundColor: Color,
+    onClick: (() -> Unit)? = null,
     modifier: Modifier = Modifier,
     textStyle: AppTextStyle = AppTextStyle.T2,
     borderColor: Color = GrayColor.C500,
     hasBorder: Boolean = true,
 ) {
     Box(
-        modifier = modifier,
+        modifier = modifier.then(
+            if (onClick != null) {
+                Modifier.clickable(role = Role.Button, onClick = onClick)
+            } else Modifier
+        ),
     ) {

As per coding guidelines: core/design-system/** — "접근성(Accessibility) 고려 여부"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/button/AppRoundButton.kt`
around lines 25 - 34, AppRoundButton currently lacks an onClick parameter and
does not set Role.Button semantics internally, forcing callers to add semantics
manually; modify the AppRoundButton signature to accept an onClick: ()->Unit
(nullable if needed) and inside the composable apply Modifier.clickable(role =
Role.Button, onClick = onClick) (or use semantics with role = Role.Button if
non-clickable variant) so the component itself manages accessibility; update
usages of modifier in AppRoundButton to chain the clickable/semantics modifier
before other modifiers and ensure text/content remains unchanged.
feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetailViewModel.kt (1)

122-122: ⚠️ Potential issue | 🟠 Major

TODO("찌르기 API 연동")는 런타임에 NotImplementedError를 발생시킵니다.

Kotlin의 TODO()는 실행 시 예외를 던지므로, 사용자가 "찌르기" 버튼을 누르면 앱이 크래시됩니다. 아직 미구현 상태라면 빈 동작이나 "준비 중" 토스트를 보여주는 것이 안전합니다.

🔧 크래시 방지 개선안
-            TaskCertificationDetailIntent.Sting -> TODO("찌르기 API 연동")
+            TaskCertificationDetailIntent.Sting -> {
+                showToast(R.string.task_certification_detail_sting_not_ready, ToastType.ERROR)
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetailViewModel.kt`
at line 122, The TaskCertificationDetailIntent.Sting branch currently calls
TODO("찌르기 API 연동") which throws NotImplementedError at runtime; replace this
TODO in the TaskCertificationDetailViewModel (the handler for
TaskCertificationDetailIntent.Sting) with a safe implementation such as a no-op
or emitting a UI effect/event (e.g., show "준비 중" toast or log and return) so the
app doesn't crash when the user taps the "찌르기" button; update the Sting intent
handler to call the appropriate method (or emit an intent/result) instead of
TODO to safely handle the unimplemented feature until the API integration is
added.
🧹 Nitpick comments (32)
feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorSideEffect.kt (1)

7-9: message: Int@StringRes 어노테이션을 추가하는 것이 좋습니다.

Int 타입만으로는 이 값이 반드시 문자열 리소스 ID여야 한다는 제약이 없습니다. 실수로 임의의 정수값이 전달될 경우, 런타임에서야 오류가 발생합니다. @StringRes를 추가하면 IDE와 lint가 컴파일 타임에 이를 검증해 줍니다.

✨ 개선 제안
+import androidx.annotation.StringRes
+
 sealed interface TaskCertificationEditorSideEffect : SideEffect {
     data class ShowToast(
-        val message: Int,
+        `@StringRes` val message: Int,
         val type: ToastType,
     ) : TaskCertificationEditorSideEffect
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorSideEffect.kt`
around lines 7 - 9, The ShowToast data class's message property is currently an
Int but not annotated, so add the `@StringRes` annotation to the message parameter
(data class ShowToast(val message: Int, ...)) to ensure IDE/lint enforce it is a
string resource ID; import androidx.annotation.StringRes and update the
constructor parameter to `@StringRes` message: Int so callers are validated at
compile time.
feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt (3)

68-88: Preview 함수의 가시성을 private으로 제한하는 것을 권장합니다.

TaskCertificationDetailTopBar 자체는 internal로 선언되어 있는데, 그 Preview 함수는 아무 수식어 없이 public입니다. Preview 함수는 IDE 전용으로만 사용되므로 private으로 제한하면 의도가 명확해지고 모듈 외부로 불필요하게 노출되는 것을 막을 수 있습니다.

💡 Preview 가시성 제한 제안
-@Preview
-@Composable
-fun TaskCertificationDetailTopBarPreview() {
+@Preview
+@Composable
+private fun TaskCertificationDetailTopBarPreview() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt`
around lines 68 - 88, The Preview function TaskCertificationDetailTopBarPreview
is currently public while the composable TaskCertificationDetailTopBar is
internal; change the preview's declaration to private (i.e., private fun
TaskCertificationDetailTopBarPreview()) so the preview is IDE-only and not
exposed outside the module, keeping the existing `@Preview` and `@Composable`
annotations and the inner usage of TwixTheme/Column unchanged.

17-17: 리소스 별칭(alias) 제거 — 미래 충돌 가능성 고려해보세요.

com.twix.designsystem.R을 별칭 없이 R로 import하면, 이 파일이 feature 모듈 자체의 리소스(com.twix.task_certification.R)를 참조해야 할 때 동일한 이름으로 충돌이 생깁니다. 기존의 DesR 별칭은 바로 이 문제를 예방하기 위한 패턴이었습니다.

현재는 ic_arrow3_leftword_modify 모두 designsystem 모듈에 있으므로 동작에는 문제가 없지만, 이후 이 파일에 feature 모듈 고유 리소스가 추가될 경우 이름 충돌로 컴파일 오류가 발생할 수 있습니다.

별칭을 유지하는 방식을 고려해보는 건 어떨까요?

💡 별칭 유지 방식 제안
-import com.twix.designsystem.R
+import com.twix.designsystem.R as DesR

그리고 사용 부분도 함께:

-                painter = painterResource(R.drawable.ic_arrow3_left),
+                painter = painterResource(DesR.drawable.ic_arrow3_left),
-                        text = stringResource(R.string.word_modify),
+                        text = stringResource(DesR.string.word_modify),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt`
at line 17, Restore a namespaced alias for the designsystem resources to avoid
future R name collisions: change the import in TaskCertificationDetailTopBar.kt
from "import com.twix.designsystem.R" to the aliased form (e.g., "import
com.twix.designsystem.R as DesR") and update any resource references that use
the plain R (notably usages of ic_arrow3_left and word_modify) to use the alias
(DesR) so they won't conflict with com.twix.task_certification.R.

43-53: noRippleClickable 람다 전달 방식의 일관성을 맞춰주세요.

Line 43에서는 noRippleClickable(onClick = onBack)처럼 함수 레퍼런스를 직접 전달하고 있지만, Line 53에서는 noRippleClickable { onClickModify() }처럼 람다로 한 번 더 감싸고 있습니다. 불필요한 래핑은 미세하게나마 recomposition 비용을 높이고, 가독성도 떨어집니다.

💡 일관된 스타일로 정리
-                            .noRippleClickable { onClickModify() },
+                            .noRippleClickable(onClick = onClickModify),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt`
around lines 43 - 53, 현재 TaskCertificationDetailTopBar의 클릭 핸들러 전달 방식이 일관되지 않습니다:
noRippleClickable(onClick = onBack)는 함수 레퍼런스를 직접 전달하는 반면 noRippleClickable {
onClickModify() }는 불필요한 래핑 람다를 사용합니다. noRippleClickable 호출들을 모두 동일하게 변경하여 레퍼런스를
직접 전달하도록 통일하세요 (예: replace the lambda form for the onClickModify usage with
noRippleClickable(onClick = onClickModify)); 참조할 심볼: noRippleClickable, onBack,
onClickModify, Modifier/Box where the modifier is applied.
feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt (1)

34-39: @Preview Composable 미제공

SwipeableCard는 공용 컴포넌트임에도 @Preview가 없어 IDE에서 렌더링을 확인할 수 없습니다. 드래그/스와이프 동작을 인터랙티브하게 미리 보기 어렵더라도, 정적 상태(기본 위치)라도 미리보기를 추가하면 레이아웃 검증과 팀원 이해에 도움이 됩니다.

💡 Preview 예시
`@Preview`(showBackground = true)
`@Composable`
private fun SwipeableCardPreview() {
    SwipeableCard(onSwipe = {}) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .background(Color.LightGray),
            contentAlignment = Alignment.Center,
        ) {
            Text("Swipe Me")
        }
    }
}

As per coding guidelines: "Preview Composable이 제공되는가?" 항목 위반.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt`
around lines 34 - 39, Add a Preview Composable for the public SwipeableCard so
IDEs can render its default/static state: create a private `@Preview` function
(e.g., SwipeableCardPreview) that calls SwipeableCard(onSwipe = {}) with a
simple static child (Box with fixed height, fillMaxWidth, background color and
centered Text) and use `@Preview`(showBackground = true) to validate layout;
ensure the preview is in the same Kotlin file as the SwipeableCard composable so
it renders correctly in the IDE.
feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CaptureCamera.kt (2)

134-134: companion object Companion — 기본 이름 명시는 불필요합니다.

Kotlin에서 컴패니언 오브젝트의 기본 이름이 Companion이므로, 굳이 명시할 필요가 없습니다. 이름을 제거하면 코드가 더 간결해집니다.

♻️ 불필요한 이름 제거
-    companion object Companion {
+    companion object {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CaptureCamera.kt`
at line 134, The companion object is explicitly named "Companion" which is
redundant; in CaptureCamera.kt remove the explicit name so the declaration uses
an unnamed companion object (change "companion object Companion" to "companion
object") to make the code more concise while keeping all existing members and
behavior in the companion.

73-83: suspendCancellableCoroutine 취소 처리가 누락되어 있습니다.

코루틴이 취소될 경우(예: 사용자가 촬영 도중 화면을 이탈), imageCapture.takePicture() 로 시작된 CameraX 작업이 중단 없이 계속 실행됩니다. continuation.invokeOnCancellation {} 을 등록하면 해당 상황에서 리소스를 정리할 수 있습니다. 현재 CameraX API에서는 진행 중인 캡처를 직접 취소하는 메서드가 없으므로, 최소한 카메라 바인딩 해제(unbind())나 로그 기록을 통해 의도를 명시하는 것이 좋습니다.

♻️ 취소 처리 추가 예시
 override suspend fun takePicture(): Result<Uri> =
     suspendCancellableCoroutine { continuation ->
         val contentValues = contentValues()
         val outputOptions = outputFileOptions(contentValues)

         imageCapture.takePicture(
             outputOptions,
             ContextCompat.getMainExecutor(context),
             capture(continuation),
         )
+
+        continuation.invokeOnCancellation {
+            // CameraX는 진행 중인 캡처를 직접 취소하는 API를 제공하지 않으나,
+            // 바인딩 해제 또는 로깅으로 취소 의도를 명시할 수 있습니다.
+            unbind()
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CaptureCamera.kt`
around lines 73 - 83, The suspendCancellableCoroutine in takePicture() is
missing cancellation handling; register continuation.invokeOnCancellation { ...
} so that when the coroutine is cancelled you stop or clean up the CameraX work
started by imageCapture.takePicture(...). Inside the cancellation handler
(referencing imageCapture, capture(continuation) and any available camera
provider or binding object) call the appropriate unbind/unbindAll method (or at
minimum log that the capture was cancelled) to release resources and avoid
orphaned capture work; ensure the handler is added alongside the
suspendCancellableCoroutine call that wraps imageCapture.takePicture().
feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CameraPreview.kt (1)

6-9: @Immutable 애너테이션이 SurfaceRequest(가변 객체)를 감싸는 클래스에 사용되고 있습니다.

@Immutable은 Compose 컴파일러에게 해당 클래스와 그 모든 공개 프로퍼티가 깊이(deep) 불변임을 보장합니다. 그러나 SurfaceRequestprovideSurface(), willNotProvideSurface() 등을 가진 가변 CameraX 객체이므로 이 계약을 충족하지 못합니다.

현재는 CaptureCamera에서 항상 새 인스턴스(CameraPreview(request))를 생성하므로 실제 동작에 문제가 없지만, Compose 컴파일러에게 잘못된 안정성 힌트를 주는 것이기 때문에 미래 코드 변경 시 예기치 않은 재구성 스킵이 발생할 수 있습니다.

단순히 안정성 힌트를 주고 싶다면 @Stable이 더 적합하거나, 컴파일러의 자동 추론에 맡기는 것을 고려해 보세요.

♻️ 개선 방법
-import androidx.compose.runtime.Immutable
-
-@Immutable
 data class CameraPreview(
     val request: SurfaceRequest,
 )

또는 더 정확한 의미를 전달하려면:

-import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable

-@Immutable
+@Stable
 data class CameraPreview(
     val request: SurfaceRequest,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CameraPreview.kt`
around lines 6 - 9, The class CameraPreview is annotated with `@Immutable` while
it wraps CameraX's mutable SurfaceRequest (which has
provideSurface()/willNotProvideSurface()), violating `@Immutable`'s deep
immutability contract; update the annotation to `@Stable` or remove the `@Immutable`
on CameraPreview and rely on Compose's inference (adjust usage in CaptureCamera
which constructs new CameraPreview(request) as needed) so the compiler isn't
given a false immutability guarantee.
feature/task-certification/src/main/java/com/twix/task_certification/editor/component/RetakeButton.kt (1)

36-41: Preview에 다크 모드 변형을 추가하면 더 좋습니다

현재 @Preview만 있어 라이트 모드만 확인할 수 있습니다. 디자인 시스템 가이드라인의 "Dark Mode 지원 여부"에 따라 @PreviewLightDark로 교체하면 두 가지 테마를 한 번에 검증할 수 있습니다.

🌙 수정 제안: `@PreviewLightDark` 적용
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewLightDark

-@Preview
+@PreviewLightDark
 `@Composable`
 private fun RetakeButtonPreview() {
     TwixTheme {
         RetakeButton(onClickRetake = {})
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/editor/component/RetakeButton.kt`
around lines 36 - 41, 현재 RetakeButtonPreview 데코레이터가 `@Preview로` 되어 있어 다크 모드 확인이
불가능합니다; RetakeButtonPreview(프리뷰 함수)를 찾아 `@Preview` 대신 디자인 시스템에서 사용하는
`@PreviewLightDark로` 교체해 라이트/다크 테마를 동시에 렌더링하도록 변경하고, 필요 시 TwixTheme 래핑이나 매개변수(예:
uiMode) 설정이 `@PreviewLightDark와` 충돌하지 않는지 확인하세요.
feature/task-certification/src/main/java/com/twix/task_certification/certification/component/TaskCertificationTopBar.kt (1)

25-41: 높이 값 하드코딩에 대해 – 선택적 개선 제안

72.dp가 직접 하드코딩되어 있는데, 디자인 시스템 차원에서 TopBar 높이를 상수로 관리하면 일관성 유지에 도움이 될 수 있습니다. 다른 TopBar 컴포넌트에서도 동일한 높이를 사용한다면, 공통 상수로 추출하는 것을 고려해 보시는 건 어떨까요?

현재 구현 자체에는 문제가 없으며, Preview도 잘 제공되어 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/component/TaskCertificationTopBar.kt`
around lines 25 - 41, The Box in TaskCertificationTopBar uses a hardcoded height
of 72.dp; extract this magic value into a shared constant (e.g., TOP_BAR_HEIGHT)
in the component or a UI constants file and replace the literal in the
height(...) call so other TopBar implementations can reuse it (update any
imports/usages and keep the modifier chain and behavior the same).
feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt (1)

41-41: TorchStatus.Companion.toggle(...) 대신 TorchStatus.toggle(...)이 더 관용적입니다

Kotlin에서 companion object 함수를 호출할 때 .Companion.을 명시적으로 붙일 필요가 없습니다. 이 변경은 기존의 더 관용적인 호출 방식에서 퇴보한 것으로 보입니다.

♻️ 개선 제안
-        val newFlashMode = TorchStatus.Companion.toggle(torch)
+        val newFlashMode = TorchStatus.toggle(torch)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt`
at line 41, The call uses the verbose form TorchStatus.Companion.toggle(torch);
replace it with the idiomatic Kotlin call TorchStatus.toggle(torch) by removing
".Companion." wherever TorchStatus.Companion.toggle(...) appears (e.g., in
TaskCertificationUiState where newFlashMode is assigned) so the companion
function is invoked in the conventional, concise way.
core/design-system/src/main/res/values/strings.xml (1)

100-100: XML 주석 닫기 태그 오타: !-->-->

Line 100과 Line 109의 주석이 !-->로 끝나고 있습니다. 기술적으로 파싱에는 문제없지만 !는 의도하지 않은 문자로 보이며, 통일성을 위해 수정하는 것이 좋습니다.

♻️ 개선 제안
-    <!-- 인증샷 촬영 화면 !-->
+    <!-- 인증샷 촬영 화면 -->
...
-    <!-- 인증샷 보기 화면 !-->
+    <!-- 인증샷 보기 화면 -->

Also applies to: 109-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/design-system/src/main/res/values/strings.xml` at line 100, Fix the
malformed XML comment closers in strings.xml where the comments for the 인증샷 촬영
화면 (and the other comment ending at the same spot) use "!-->" instead of the
correct "-->"; locate the comment nodes containing "인증샷 촬영 화면" (and the one at
the other occurrence) and replace the closing token "!-->" with the valid XML
comment terminator "-->" so comments are syntactically consistent.
core/network/src/main/java/com/twix/network/model/request/photolog/model/PhotologModifyRequest.kt (2)

1-1: 패키지 경로에 model이 중복됩니다

com.twix.network.model.request.photolog.modelmodel이 외부(network.model)와 내부(photolog.model) 두 곳에 등장합니다. com.twix.network.model.request.photolog로 정리하는 것이 더 간결하고 일관성 있는 경로입니다.

♻️ 패키지 경로 개선 제안
-package com.twix.network.model.request.photolog.model
+package com.twix.network.model.request.photolog
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/network/src/main/java/com/twix/network/model/request/photolog/model/PhotologModifyRequest.kt`
at line 1, The package declaration in PhotologModifyRequest.kt redundantly
contains "model" twice; change the package from
com.twix.network.model.request.photolog.model to
com.twix.network.model.request.photolog and update the file's package line
accordingly, then refactor any imports/usages referencing
com.twix.network.model.request.photolog.model to the new package (search for
PhotologModifyRequest and adjust references) so all declarations and consumers
compile with the consolidated package name.

9-9: @SerialName("comment")는 필드명과 동일하므로 생략 가능합니다

kotlinx.serialization에서 @SerialName의 값이 필드명과 동일할 때는 어노테이션이 없어도 동작이 같습니다. 노이즈를 줄이기 위해 생략을 고려해보세요.

♻️ 불필요한 어노테이션 제거 제안
 `@Serializable`
 data class PhotologModifyRequest(
     `@SerialName`("fileName") val fileName: String,
-    `@SerialName`("comment") val comment: String,
+    val comment: String,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/network/src/main/java/com/twix/network/model/request/photolog/model/PhotologModifyRequest.kt`
at line 9, The `@SerialName`("comment") annotation on the PhotologModifyRequest
property is redundant because the serialized name matches the Kotlin property
name; remove the annotation on the property declaration (the
`@SerialName("comment") val comment: String` line) so the serializer uses the
default name and reduces noise while leaving the property and class unchanged.
core/navigation/src/main/java/com/twix/navigation/savedstate/SafeDecode.kt (2)

9-22: null 체크를 try 블록 밖으로 분리하면 로그 가독성이 향상됩니다

현재 error("Missing nav arg: $key")try 블록 내부에 있어서, rawnull인 경우에도 catch가 이를 잡아 "decode failed" 로 기록합니다. "누락된 인자" 오류와 "JSON 디코딩 실패" 오류가 같은 메시지로 보이면 디버깅 시 혼란을 줄 수 있습니다.

♻️ null 체크 분리 제안
 inline fun <reified T> SavedStateHandle.decodeNavArgs(key: String): T {
-    val raw = get<String>(key)
-
+    val raw = get<String>(key) ?: error("Missing nav arg: $key")
     try {
-        val decoded =
-            raw?.let(Uri::decode)
-                ?: error("Missing nav arg: $key")
-
+        val decoded = Uri.decode(raw)
         Logger.d { "NavArgs[$key] decoded=$decoded" }
-
         return Json.decodeFromString(decoded)
     } catch (e: Exception) {
         Logger.e(e) { "NavArgs[$key] decode failed raw=$raw" }
         throw e
     }
 }

이렇게 하면 try 블록은 실제 URI 디코딩·JSON 역직렬화 실패만 처리하고, null 누락 케이스는 별도로 즉시 크래시를 냅니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/navigation/src/main/java/com/twix/navigation/savedstate/SafeDecode.kt`
around lines 9 - 22, The null-check for the nav argument should be moved out of
the try block so missing-arg errors are logged/raised separately from
decode/deserialization failures: after retrieving raw via get<String>(key) check
raw == null and call error("Missing nav arg: $key") (so the missing-arg case
never enters the try), then wrap only the Uri::decode and Json.decodeFromString
calls (using Uri::decode, Logger.d, Json.decodeFromString) in the try/catch to
ensure Logger.e in the catch only reports true decode/deserialization errors for
raw values that existed.

18-18: 기본 Json 인스턴스는 ignoreUnknownKeys = false로 설정되어 있습니다

현재 코드는 Json.decodeFromString(decoded)로 기본 Json 인스턴스를 사용 중입니다. 이 경우 JSON에 역직렬화 대상 클래스에 없는 필드가 있으면 SerializationException이 발생합니다.

프로젝트에서는 이미 여러 곳에서 ignoreUnknownKeys = true로 설정된 공통 Json 인스턴스를 사용하고 있습니다:

  • HttpClientProvider.kt (line 44)
  • AuthConfigure.kt (line 20)
  • ApiCall.kt (line 15)

이 패턴을 따라 SafeDecode에서도 프로젝트의 기본 Json 설정을 사용하거나, 필요시 설정된 Json 인스턴스를 주입받도록 개선하면 어떨까요? 이렇게 하면 앱 내 데이터 처리의 일관성을 유지할 수 있고, 향후 스키마 변경에도 더 안전해집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/navigation/src/main/java/com/twix/navigation/savedstate/SafeDecode.kt`
at line 18, The current SafeDecode.kt uses the default
Json.decodeFromString(decoded) which throws on unknown fields; update SafeDecode
to use the project's shared Json configuration that has ignoreUnknownKeys = true
(or accept a Json instance via parameter/injection) instead of the default Json
object so deserialization is tolerant of extra fields; change the call site in
the SafeDecode function to use the injected/shared Json instance (refer to the
SafeDecode function and the return line using Json.decodeFromString) so it
aligns with the Json instances used in HttpClientProvider, AuthConfigure, and
ApiCall.
core/design-system/src/main/java/com/twix/designsystem/components/photolog/CertificatedCard.kt (2)

37-37: comment?.isNotEmpty() == true!comment.isNullOrEmpty()로 간결하게 표현할 수 있습니다.

두 표현 모두 동일하게 동작하지만, Kotlin 표준 라이브러리의 isNullOrEmpty() 확장 함수를 사용하면 의도가 더 명확하고 관용적입니다.

♻️ 개선 제안
-        if (comment?.isNotEmpty() == true) {
+        if (!comment.isNullOrEmpty()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/photolog/CertificatedCard.kt`
at line 37, Replace the verbose null-check in CertificatedCard (the conditional
using comment?.isNotEmpty() == true) with the idiomatic Kotlin check using
comment.isNullOrEmpty() (negated) — i.e., change the condition to use
!comment.isNullOrEmpty() so the intent is clearer and more concise wherever that
expression appears in the CertificatedCard composable/logic.

26-36: 인증샷 이미지에 contentDescription을 제공하는 것이 좋습니다.

인증샷 이미지는 카드의 핵심 콘텐츠입니다. 스크린 리더 사용자를 위해 적절한 contentDescription을 추가하면 접근성이 향상됩니다. 디자인 시스템 리뷰 가이드에서 접근성(Accessibility) 고려를 권장하고 있습니다. As per coding guidelines, core/design-system/** 파일은 접근성(Accessibility) 고려 여부를 검토해야 합니다.

♻️ 개선 제안
         AsyncImage(
             model =
                 ImageRequest
                     .Builder(LocalContext.current)
                     .data(imageUrl)
                     .crossfade(true)
                     .build(),
-            contentDescription = null,
+            contentDescription = "인증샷 이미지",
             contentScale = ContentScale.Crop,
             modifier = Modifier.fillMaxSize(),
         )

또는 외부에서 contentDescription 파라미터를 전달받도록 하면 더 유연합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/photolog/CertificatedCard.kt`
around lines 26 - 36, The AsyncImage call inside CertificatedCard is missing a
contentDescription which harms accessibility; update the CertificatedCard
composable to accept a contentDescription: String? parameter (or a descriptive
default) and pass that value into AsyncImage's contentDescription argument,
ensuring the composable signature (CertificatedCard(...)) and the AsyncImage
call are updated accordingly so callers can provide a meaningful description for
the certification image.
core/design-system/src/main/java/com/twix/designsystem/components/photolog/ForegroundCard.kt (1)

34-38: stringResource의 포맷 인자 전달 방식을 개선할 수 있습니다.

현재 stringResource(...).format(nickName) 패턴을 사용하고 있는데, Compose에서는 stringResource(resId, formatArgs)를 직접 사용하는 것이 더 관용적(idiomatic)입니다. stringResource 오버로드가 내부적으로 포맷팅을 처리하므로 불필요한 중간 String 생성을 줄이고, 의도가 더 명확해집니다.

♻️ 개선 제안
                        BetweenUs.PARTNER ->
-                            stringResource(R.string.partner_not_task_certification).format(
-                                nickName,
-                            )
+                            stringResource(R.string.partner_not_task_certification, nickName)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/photolog/ForegroundCard.kt`
around lines 34 - 38, The string formatting for the BetweenUs.PARTNER branch in
ForegroundCard.kt uses stringResource(...).format(nickName); change this to use
the stringResource overload that accepts format args directly
(stringResource(R.string.partner_not_task_certification, nickName)) so Compose
performs the formatting without creating an intermediate String; locate the
mapping for BetweenUs.PARTNER and replace the current .format(...) usage with
passing nickName as the second argument to stringResource.
core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt (1)

70-77: 접근성(Accessibility) 검토: contentDescription = null이 적절한지 확인해 주세요.

ic_keepi_sting 이미지가 순수 장식용(decorative)이라면 contentDescription = null은 올바릅니다. 다만 이 이미지가 사용자에게 의미 있는 정보를 전달한다면 적절한 설명을 추가하는 것이 좋습니다. 디자인 시스템 가이드에서 접근성 고려를 권장하고 있으므로 확인 부탁드립니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt`
around lines 70 - 77, The Image in BackgroundCard.kt uses imageVector =
R.drawable.ic_keepi_sting with contentDescription = null which may hide
meaningful info from assistive tech; verify whether ic_keepi_sting is purely
decorative—if it conveys information, replace the null with a localized
description via a string resource (e.g., provide a descriptive string from
resources) and set contentDescription accordingly on the Image composable; if it
is purely decorative, add a comment explaining why contentDescription is null to
make the intent explicit.
feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionBar.kt (1)

81-86: 매직 넘버 4 대신 동적으로 마지막 인덱스를 계산하는 것이 안전합니다.

현재 index < 4ReactionUiModel.entries가 정확히 5개라고 가정합니다. 향후 리액션 타입이 추가되거나 제거되면 구분선(divider) 렌더링이 깨질 수 있습니다. 방어적으로 entries.lastIndex를 활용하면 항목 수 변경에도 안전합니다.

♻️ 개선 제안
-                if (index < 4) {
+                if (index < ReactionUiModel.entries.lastIndex) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionBar.kt`
around lines 81 - 86, The divider rendering uses a hard-coded magic number
("index < 4") which assumes ReactionUiModel.entries has exactly five items;
change the condition to compare against the actual last index (use
ReactionUiModel.entries.lastIndex or entries.size - 1) so that in the
ReactionBar (where the loop uses index) you only render the VerticalDivider when
index < entries.lastIndex, making divider rendering resilient to added/removed
reaction entries.
core/navigation/src/main/java/com/twix/navigation/args/TaskCertificationNavArgs.kt (1)

17-24: DetailNavArgsphotologId 기본값 -1은 매직 넘버입니다.

-1이라는 값이 "미설정"을 의미하는 것으로 보이는데, 이 의도가 코드에서 명시적으로 드러나지 않습니다. 이후 이 값을 사용하는 쪽에서 -1인지 비교하는 로직이 흩어질 수 있어 유지보수에 불리합니다.

두 가지 방법을 제안합니다:

  1. photologIdLong?(nullable)로 변경하여 "값이 없음"을 타입 시스템으로 표현
  2. 혹은 companion object에 상수를 정의하여 의미를 명시
♻️ 제안 (옵션 2 — 상수 정의)
 `@Serializable`
 data class DetailNavArgs(
     val goalId: Long,
     val from: NavRoutes.TaskCertificationRoute.From,
-    val photologId: Long = -1,
+    val photologId: Long = UNSET_PHOTOLOG_ID,
     val selectedDate: String = "",
     val comment: String = "",
-)
+) {
+    companion object {
+        const val UNSET_PHOTOLOG_ID = -1L
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/navigation/src/main/java/com/twix/navigation/args/TaskCertificationNavArgs.kt`
around lines 17 - 24, The current DetailNavArgs data class uses a magic-number
default of -1 for photologId; change photologId to a nullable Long (photologId:
Long? = null) in DetailNavArgs so "unset" is represented by the type system,
then update all call sites and consumers that access DetailNavArgs.photologId
(e.g., any code referencing NavRoutes.TaskCertificationRoute.From and places
that compare photologId to -1) to handle null instead of comparing to -1 (use
safe calls, elvis, or explicit null checks) and ensure
serialization/deserialization logic accepts a nullable photologId.
feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt (1)

53-63: navigateToCertificationDetail도 동일한 패턴으로 리팩터링 하는 것을 고려해 볼 수 있습니다.

현재 navigateToCertificationDetail은 여전히 goalId, date, betweenUs.name을 개별 인자로 전달하고 있습니다. navigateToCertificationDetailNavArgs로 통합된 것처럼, 이쪽도 유사한 NavArgs 패턴을 적용하면 네비게이션 인자 전달 방식이 일관적으로 유지됩니다. 물론 현재 라우트(TaskCertificationDetailRoute)가 다른 인자 구조를 사용하고 있다면 별도 작업이 필요할 수 있으니, 추후 리팩터링 시 고려해 주시면 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt` around
lines 53 - 63, 현재 navigateToCertificationDetail 콜이 goalId, date, betweenUs.name을
개별 인자로 전달해 네비게이션 인자 방식이 일관되지 않습니다; navigateToCertification에서 사용한 DetailNavArgs
패턴을 동일하게 적용하도록 수정하세요: TaskCertificationDetailRoute.createRoute의 시그니처를
DetailNavArgs(또는 적절한 CertificationDetailNavArgs) 객체를 받도록 변경하거나 해당 객체를 새로 생성한 뒤
navigateToCertificationDetail 내부에서 그 객체를 생성해 전달하고
navController.navigate(destination) 호출은 기존처럼 유지하지만 인자 묶음을 사용하도록 업데이트하세요 (참조 심볼:
navigateToCertificationDetail, navigateToCertification, DetailNavArgs,
TaskCertificationDetailRoute.createRoute, navController.navigate).
core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt (1)

13-17: canUploadvalue.isNotEmpty() 조건이 불필요합니다.

hasMaxCommentLengthtrue이면(value.length == 5) value.isNotEmpty()는 항상 true이므로, 해당 조건은 논리적으로 중복됩니다. 제거하면 가독성이 향상됩니다.

♻️ 간소화 제안
     val canUpload: Boolean
         get() =
-            value.isEmpty() ||
-                value.isNotEmpty() &&
-                hasMaxCommentLength
+            value.isEmpty() || hasMaxCommentLength
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt`
around lines 13 - 17, The canUpload getter contains a redundant
value.isNotEmpty() check; simplify its logic by replacing the current expression
with a direct boolean combining value.isEmpty() and hasMaxCommentLength (i.e.,
return value.isEmpty() || hasMaxCommentLength) so the getter for canUpload (in
CommentUiModel) is clearer and equivalent.
feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt (1)

77-85: toSerializer() 메서드 명이 실제 역할과 다소 괴리가 있어 보입니다.

이 메서드는 UI 상태를 EditorNavArgs로 변환하는 역할을 합니다. toSerializer라는 이름은 직렬화 로직 자체를 반환하는 것처럼 읽힐 수 있어, toEditorNavArgs()와 같이 반환 타입을 반영한 이름이 의도를 더 명확히 전달할 수 있습니다.

또한 myPhotolognull일 때 photologId = -1, imageUrl = ""로 폴백되는데, canModifytrue일 때만 호출된다면 실질적인 문제는 없지만, 방어적으로 호출 가능 조건을 문서화하거나 require(myPhotolog != null) 같은 사전 조건을 추가하는 것도 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt`
around lines 77 - 85, Rename the misleading toSerializer() to toEditorNavArgs()
to reflect that it converts the UI state into an EditorNavArgs instance; update
all call sites to use toEditorNavArgs(). Additionally make the null-contract
explicit by adding a precondition check such as require(myPhotolog != null) (or
document that this is only called when canModify == true) before building
EditorNavArgs so photologId/imageUrl aren’t silently defaulted, and reference
the EditorNavArgs, toSerializer (old name), toEditorNavArgs (new name),
myPhotolog, and canModify symbols when making the change.
core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentAnchorFrame.kt (2)

40-104: 키보드 대응 로직이 잘 분리되어 재사용 가능한 컴포넌트로 구현되었습니다.

WindowInsets.ime를 활용한 키보드 감지, AnimatedVisibility를 통한 딤 처리, 동적 offset 계산 등 복잡한 로직이 하나의 컴포저블로 깔끔하게 캡슐화되어 있습니다. 기존 TaskCertificationScreen에서 인라인으로 존재하던 로직을 design-system으로 분리한 것은 좋은 리팩터링입니다.

한 가지 제안: 디자인 시스템 모듈의 컴포넌트로서 @Preview 함수를 제공하면 UI 확인과 유지보수에 도움이 됩니다. As per coding guidelines, "Preview Composable이 제공되는가?"

♻️ Preview 추가 제안
`@Preview`
`@Composable`
private fun CommentAnchorFramePreview() {
    CommentAnchorFrame(
        uiModel = CommentUiModel(),
        anchorBottom = 800f,
        onCommentChanged = {},
        onFocusChanged = {},
    )
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentAnchorFrame.kt`
around lines 40 - 104, Add a Preview composable for the CommentAnchorFrame so
designers/devs can quickly inspect it in Android Studio; create a private
`@Preview` `@Composable` function (e.g., CommentAnchorFramePreview) that calls
CommentAnchorFrame with a default CommentUiModel instance, a sensible
anchorBottom (e.g., 800f), and no-op lambdas for onCommentChanged and
onFocusChanged, and mark it private so it lives alongside the existing
CommentAnchorFrame composable for design-system previews.

48-48: anchorBottom == 0f 비교는 "아직 측정되지 않은 상태"의 센티넬 값으로 사용되고 있는 것으로 보입니다.

Float의 == 비교는 일반적으로 부동소수점 정밀도 문제로 권장되지 않지만, 여기서는 mutableFloatStateOf(0f)의 초기값과의 정확한 비교이므로 의도대로 동작합니다. 다만, 의도를 더 명확히 하기 위해 nullable(Float?)로 변경하고 null 체크를 하는 방안도 고려해볼 수 있습니다. 현재 구현도 문제없으므로 참고 수준입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentAnchorFrame.kt`
at line 48, The sentinel check using exact float equality on anchorBottom (if
(anchorBottom == 0f) return) should be made explicit by turning anchorBottom
into a nullable Float sentinel: change the state backing anchorBottom (where
mutableFloatStateOf(0f) is used) to a nullable state (e.g.,
mutableStateOf<Float?>(null)), update any assignments to set the measured float
value, and replace the equality check with a null check (if (anchorBottom ==
null) return) so intent is clearer and avoids ambiguous float-equality
semantics; update usages of anchorBottom to handle the non-null Float after the
null check.
feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorViewModel.kt (1)

80-85: currentState에서 직접 값을 읽어 API를 호출하는 부분에 대해 한 가지 고려 사항이 있습니다.

launchModifyComment()가 호출되는 시점과 실제 실행 시점 사이에 currentState가 변경될 가능성은 낮지만, launchResultblock 람다가 비동기로 실행되므로 호출 시점의 상태 값을 캡처해두는 것이 방어적으로 더 안전할 수 있습니다.

현재 구조에서 실질적인 문제가 발생할 가능성은 낮으므 참고 수준의 제안입니다.

♻️ 방어적 코딩 제안
 private fun modifyComment() {
+    val state = currentState
     if (currentState.comment.canUpload.not()) {
         showToast(R.string.comment_error_message, ToastType.ERROR)
     } else if (currentState.isCommentNotChanged) {
         showToast(R.string.task_certification_editor_not_modified, ToastType.ERROR)
     } else {
         launchResult(
-            block = { launchModifyComment() },
+            block = {
+                photologRepository.modifyPhotolog(
+                    state.photologId,
+                    state.imageName,
+                    state.comment.value,
+                )
+            },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorViewModel.kt`
around lines 80 - 85, The code reads currentState properties directly inside
launchModifyComment which risks using mutated state when the async block runs;
update launchModifyComment to first capture needed values into local vals (e.g.,
val photologId = currentState.photologId, val imageName =
currentState.imageName, val comment = currentState.comment.value) and then call
photologRepository.modifyPhotolog(photologId, imageName, comment) so the
repository call uses the snapshot of state captured at call time (this also
covers cases where this method is invoked from within a launchResult block).
core/network/src/main/java/com/twix/network/service/PhotoLogService.kt (1)

27-30: @Query 파라미터로 LocalDate 대신 String을 사용하여 코드베이스 일관성을 맞추세요.

GoalService에서는 날짜 쿼리 파라미터를 String으로 정의하고 있으나, PhotoLogService에서만 LocalDate를 직접 사용하고 있습니다. LocalDate.toString()"yyyy-MM-dd" 형식을 반환하므로 동작하겠지만, 코드베이스 전체의 패턴과 일치시키는 것이 유지보수성을 높입니다.

또한 파라미터 이름을 @Query 어노테이션과 일치시켜야 합니다. 현재 @Query("targetDate") request: LocalDate로 되어 있으나, 파라미터명은 targetDate로 변경하는 것이 관례를 따릅니다.

`@GET`("api/v1/photologs")
suspend fun fetchPhotoLogs(
    `@Query`("targetDate") targetDate: String,
): PhotoLogsResponse

호출 시 LocalDate를 전달해야 한다면, 인자 변환은 호출부에서 .toString()으로 처리하는 것이 명시적입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/network/src/main/java/com/twix/network/service/PhotoLogService.kt`
around lines 27 - 30, Change the fetchPhotoLogs signature in PhotoLogService so
the `@Query` parameter uses a String and the parameter name matches the query key:
replace request: LocalDate with targetDate: String in the suspend fun
fetchPhotoLogs method; this aligns with GoalService's pattern and keeps
date-to-string conversion explicit (callers should pass LocalDate.toString()
when needed).
core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt (1)

38-49: TaskCertificationDetailRoute만 기존 다중 경로 파라미터 방식을 유지하고 있습니다.

TaskCertificationRouteTaskCertificationEditorRoute{data} 기반 JSON 인코딩 방식으로 전환되었는데, TaskCertificationDetailRoute는 여전히 {goalId}/{date}/{betweenUs} 방식을 사용하고 있습니다. 현재 기능상 문제는 없지만, 일관성을 위해 향후 마이그레이션을 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt` around lines
38 - 49, TaskCertificationDetailRoute currently uses multiple path params
("task_certification_detail/{goalId}/{date}/{betweenUs}") while
TaskCertificationRoute and TaskCertificationEditorRoute use a single "{data}"
JSON-encoded param; to align them, refactor TaskCertificationDetailRoute (object
TaskCertificationDetailRoute and its createRoute) to accept a single
JSON-encoded data string param (e.g., "{data}") and update createRoute to
serialize the goalId, date, and betweenUs into that JSON string, ensuring ARG
constants reflect the new "data" arg name and parsing/decoding is implemented
where this route is consumed.
feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorRoute.kt (1)

125-133: TaskCertificationEditorScreen의 가시성(visibility)을 internal 또는 private으로 제한하는 것을 권장합니다.

이 Screen 컴포저블은 모듈 외부에서 직접 사용될 필요가 없고, TaskCertificationEditorRoute를 통해서만 접근하면 됩니다. internal 가시성으로 제한하면 API surface를 줄이고 모듈 경계를 명확히 할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorRoute.kt`
around lines 125 - 133, Limit the visibility of the
TaskCertificationEditorScreen composable to the module by changing its
declaration from public to internal (or private if only used within the same
file); update the function signature for TaskCertificationEditorScreen
accordingly and ensure callers use TaskCertificationEditorRoute to access the
screen so the composable is no longer exposed from the module API.
core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt (1)

100-105: 접근성(Accessibility) 관련 고려가 필요합니다.

숨겨진 TextField(width=0, alpha=0)와 커스텀 원형 UI로 구성되어 있어, 스크린 리더 사용자가 이 컴포넌트를 인식하고 상호작용하기 어려울 수 있습니다. Boxsemantics modifier를 추가하여 접근성 정보를 제공하는 것을 권장합니다.

As per coding guidelines: "접근성(Accessibility) 고려 여부"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt`
around lines 100 - 105, The Box using modifier.noRippleClickable (around the
custom circular UI that focuses the hidden TextField via focusRequester) lacks
accessibility semantics; update the Box to include a semantics modifier that
exposes an accessible label/role and clickable state (e.g., contentDescription
or text, role = Button or TextField as appropriate, and
stateDescription/disabled if needed) and connect it to the hidden TextField (use
onClick semantics to request focus) so screen readers can discover and interact
with the control; locate the Box declaration and the focusRequester usage in
CommentTextField.kt and add the appropriate semantics modifiers to mirror the
visible UI behavior.

@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 23, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":404,"request":{"method":"PATCH","url":"https://api.github.com/repos/YAPP-Github/Twix-Android/issues/comments/3942206587","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\n이 PR은 작업 인증 관련 기능을 대규모로 리팩토링합니다. 네비게이션을 경로 파라미터에서 JSON 직렬화 인자로 변경하고 TaskCertificationEditor(편집) 화면을 추가했습니다. Comment UI를 개선하여 앵커형 배치로 변경하고 CommentUiModel 필드명(comment → value)을 정규화했습니다. photolog 관련 명칭(uploadPhotoLog → uploadPhotolog)과 API(로컬 날짜 사용)·패키지 구조(contract/component)도 재정렬되었으며 문자열 리소스는 feature 모듈에서 design-system으로 이동했습니다.\n\n## Estimated code review effort\n\n🎯 5 (Critical) | ⏱️ ~120 minutes\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 2 | ❌ 1</summary>\n\n### ❌ Failed checks (1 warning)\n\n|     Check name     | Status     | Explanation                                                                          | Resolution                                                                         |\n| :----------------: | :--------- | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- |\n| 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. |\n\n<details>\n<summary>✅ Passed checks (2 passed)</summary>\n\n|     Check name    | Status   | Explanation                                                        |\n| :---------------: | :------- | :----------------------------------------------------------------- |\n|    Title check    | ✅ Passed | PR 제목이 주요 변경사항인 인증샷 수정 화면 UI 구현과 API 연동을 명확히 설명합니다.                |\n| Description check | ✅ Passed | PR 설명이 작업 내용(인증샷 수정 기능, 키보드 대응 컴포저블 분리, 디자인시스템 이동)과 결과물 영상을 제공합니다. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings (stacked PR)\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> 📝 Generate docstrings (commit on current branch)\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `feat/#85-task-certification-update`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=YAPP-Github/Twix-Android&utm_content=87)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/YAPP-Github/Twix-Android/issues/comments/3942206587","status":404,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","content-encoding":"gzip","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Mon, 23 Feb 2026 05:06:25 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","transfer-encoding":"chunked","vary":"Accept-Encoding, Accept, X-Requested-With","x-accepted-github-permissions":"issues=write; pull_requests=write","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"0454:2179B4:67ED9AC:1C77C50F:699BE050","x-ratelimit-limit":"12500","x-ratelimit-remaining":"12492","x-ratelimit-reset":"1771826535","x-ratelimit-resource":"core","x-ratelimit-used":"8","x-xss-protection":"0"},"data":{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#update-an-issue-comment","status":"404"}}}

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

Caution

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

⚠️ Outside diff range comments (1)
feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt (1)

92-112: ⚠️ Potential issue | 🟡 Minor

BetweenUs.valueOf(betweenUs)에서 유효하지 않은 문자열이 전달되면 IllegalArgumentException이 발생합니다.

Line 104의 BetweenUs.valueOf(betweenUs)는 네비게이션 인자로 전달받은 문자열을 enum으로 변환하는데, 값이 유효하지 않으면 런타임 크래시로 이어집니다. 네비게이션 인자는 외부 입력이므로 방어 로직이 있으면 안정성이 높아집니다.

runCatching이나 BetweenUs.entries.find { it.name == betweenUs } 패턴으로 안전하게 파싱하는 것을 고려해 보시면 좋겠습니다.

🛡️ 안전한 enum 변환 예시
     return TaskCertificationDetailUiState(
         goalId = goalId,
-        currentShow = BetweenUs.valueOf(betweenUs),
+        currentShow = BetweenUs.entries.firstOrNull { it.name == betweenUs } ?: BetweenUs.PARTNER,
         selectedDate = selectedDate,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt`
around lines 92 - 112, The current PhotoLogs.toUiState function calls
BetweenUs.valueOf(betweenUs) which will throw IllegalArgumentException for
invalid nav input; replace that unsafe call with a safe parse (e.g.,
BetweenUs.entries.find { it.name == betweenUs } or runCatching {
BetweenUs.valueOf(betweenUs) }.getOrNull()) and supply a sensible fallback (use
an appropriate default enum constant from BetweenUs) before assigning to
currentShow so malformed navigation strings no longer crash the app.
🧹 Nitpick comments (3)
feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationCardContent.kt (1)

27-32: buttonTitle 결정 로직이 다른 부분과 일관성이 없습니다.

Line 31-32에서는 uiState.isDisplayedMyPhotolog를 활용하여 ME/PARTNER 분기를 깔끔하게 처리하고 있는데, Line 27-30의 buttonTitle은 여전히 uiState.currentShow == BetweenUs.ME를 직접 비교하고 있습니다.

동일한 의미의 분기를 한 곳은 computed property로, 다른 곳은 raw enum 비교로 처리하면 추후 조건이 변경될 때 누락 가능성이 있습니다. isDisplayedMyPhotolog로 통일하면 일관성이 높아질 것 같은데, 의도적으로 분리하신 건지 확인 부탁드립니다.

♻️ 일관성 개선 제안
             buttonTitle =
-                when (uiState.currentShow) {
-                    BetweenUs.ME -> stringResource(R.string.task_certification_take_picture)
-                    BetweenUs.PARTNER -> stringResource(R.string.task_certification_detail_partner_sting)
-                },
+                if (uiState.isDisplayedMyPhotolog) {
+                    stringResource(R.string.task_certification_take_picture)
+                } else {
+                    stringResource(R.string.task_certification_detail_partner_sting)
+                },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationCardContent.kt`
around lines 27 - 32, The buttonTitle branch uses uiState.currentShow ==
BetweenUs.ME while rotation and onClick use uiState.isDisplayedMyPhotolog,
causing inconsistency; update the buttonTitle logic in
TaskCertificationCardContent (the code that sets buttonTitle) to use
uiState.isDisplayedMyPhotolog for its ME/PARTNER decision (mirroring rotation
and onClick), keeping the same stringResource choices and preserving handlers
onClickUpload and onClickSting so all three decisions (buttonTitle, rotation,
onClick) derive from the same uiState.isDisplayedMyPhotolog condition.
feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt (2)

81-89: toSerializer() 호출 시 myPhotolog가 null인 경우의 방어 로직을 확인해 주세요.

Line 86에서 myPhotolog?.photologId ?: -1로 sentinel value를 사용하고 있습니다. toSerializer()canModify == true(즉, myPhotolog != null) 상태에서만 호출된다면 안전하지만, 만약 null 상태에서 호출되면 -1이 Editor 화면으로 전달됩니다.

방어적으로 호출부에서 canModify 체크가 보장되는지 확인하시거나, 혹은 toSerializer()의 반환 타입을 EditorNavArgs?로 변경하여 null-safety를 컴파일 타임에 보장하는 방법도 고려해 볼 수 있습니다.

💡 Nullable 반환 타입 대안
-    fun toSerializer() =
-        EditorNavArgs(
+    fun toSerializer(): EditorNavArgs? {
+        val photolog = myPhotolog ?: return null
+        return EditorNavArgs(
             goalId = goalId,
             nickname = myNickname,
             goalName = goalName,
-            photologId = myPhotolog?.photologId ?: -1,
-            imageUrl = myPhotolog?.imageUrl ?: "",
-            comment = myPhotolog?.comment,
+            photologId = photolog.photologId,
+            imageUrl = photolog.imageUrl,
+            comment = photolog.comment,
         )
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt`
around lines 81 - 89, toSerializer() currently uses sentinel values when
myPhotolog is null which can leak invalid data to the Editor; change the
signature of toSerializer() to return EditorNavArgs? (nullable) and inside
return null when myPhotolog == null (or alternatively require(canModify) at the
top if you prefer a hard failure), and then update all callers to handle the
nullable return (or ensure canModify is checked before calling). Reference
symbols: toSerializer(), myPhotolog, EditorNavArgs, canModify.

69-79: canModifycanReaction에서도 isDisplayedMyPhotolog를 활용하면 일관성이 높아집니다.

isDisplayedMyPhotolog를 새로 도입하셨는데, 바로 아래의 canModify(Line 74)와 canReaction(Line 78)에서는 여전히 currentShow == BetweenUs.ME / BetweenUs.PARTNER를 직접 비교하고 있습니다. 동일한 의미의 조건이므로 통일하면 추후 BetweenUs 분기 조건이 변경될 때 한 곳만 수정하면 되어 유지보수에 유리합니다.

♻️ 일관성 개선 제안
     val canModify: Boolean
         get() =
-            currentShow == BetweenUs.ME && isDisplayedGoalCertificated
+            isDisplayedMyPhotolog && isDisplayedGoalCertificated

     val canReaction: Boolean
         get() =
-            currentShow == BetweenUs.PARTNER && isDisplayedGoalCertificated
+            !isDisplayedMyPhotolog && isDisplayedGoalCertificated
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt`
around lines 69 - 79, Refactor the boolean checks to use shared helpers: add a
new computed property isDisplayedPartnerPhotolog (val
isDisplayedPartnerPhotolog: Boolean get() = currentShow == BetweenUs.PARTNER)
and change canModify to use isDisplayedMyPhotolog && isDisplayedGoalCertificated
and canReaction to use isDisplayedPartnerPhotolog && isDisplayedGoalCertificated
(replace direct currentShow == BetweenUs.* comparisons with the new helpers) so
the BetweenUs branching is centralized via isDisplayedMyPhotolog /
isDisplayedPartnerPhotolog.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt`:
- Around line 151-168: The uploadPhotolog function currently calls
LocalDate.parse(navArgs.selectedDate) inside the launchResult block which can
throw DateTimeParseException and crash; before calling launchResult, validate or
safely parse navArgs.selectedDate (e.g., check for blank and attempt parsing
inside a try/catch) and if parsing fails show the same error toast and return
early; then pass the successfully parsed verificationDate into PhotologParam
when calling photologRepository.uploadPhotolog so uploadPhotolog,
navArgs.selectedDate, LocalDate.parse, PhotologParam, launchResult and
handleUploadPhotologSuccess are updated accordingly to avoid unhandled parsing
exceptions.

---

Outside diff comments:
In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt`:
- Around line 92-112: The current PhotoLogs.toUiState function calls
BetweenUs.valueOf(betweenUs) which will throw IllegalArgumentException for
invalid nav input; replace that unsafe call with a safe parse (e.g.,
BetweenUs.entries.find { it.name == betweenUs } or runCatching {
BetweenUs.valueOf(betweenUs) }.getOrNull()) and supply a sensible fallback (use
an appropriate default enum constant from BetweenUs) before assigning to
currentShow so malformed navigation strings no longer crash the app.

---

Duplicate comments:
In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt`:
- Around line 181-198: The onSuccess path in modifyPhotolog currently calls
detailRefreshBus.notifyChanged(TaskCertificationRefreshBus.Publisher.PHOTOLOG)
which triggers unnecessary goal list refreshes; change the publisher to
TaskCertificationRefreshBus.Publisher.EDITOR in the modifyPhotolog function
(inside the onSuccess lambda) so only editor-related subscribers are notified
when photolog is modified, keeping the existing
tryEmitSideEffect(TaskCertificationSideEffect.NavigateToDetail) and error
handling intact.
- Around line 170-179: The EDITOR branch in handleUploadPhotologSuccess() is
unreachable because upload() calls modifyPhotolog() for
NavRoutes.TaskCertificationRoute.From.EDITOR, so remove the dead branch or make
it defensive: replace the NavRoutes.TaskCertificationRoute.From.EDITOR -> Unit
branch with a clear error/log (e.g., processLogger.error or throw
IllegalStateException) to surface unexpected calls; update
handleUploadPhotologSuccess() and add a brief comment referencing upload() and
NavRoutes.TaskCertificationRoute.From to explain why the branch was changed.

---

Nitpick comments:
In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationCardContent.kt`:
- Around line 27-32: The buttonTitle branch uses uiState.currentShow ==
BetweenUs.ME while rotation and onClick use uiState.isDisplayedMyPhotolog,
causing inconsistency; update the buttonTitle logic in
TaskCertificationCardContent (the code that sets buttonTitle) to use
uiState.isDisplayedMyPhotolog for its ME/PARTNER decision (mirroring rotation
and onClick), keeping the same stringResource choices and preserving handlers
onClickUpload and onClickSting so all three decisions (buttonTitle, rotation,
onClick) derive from the same uiState.isDisplayedMyPhotolog condition.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt`:
- Around line 81-89: toSerializer() currently uses sentinel values when
myPhotolog is null which can leak invalid data to the Editor; change the
signature of toSerializer() to return EditorNavArgs? (nullable) and inside
return null when myPhotolog == null (or alternatively require(canModify) at the
top if you prefer a hard failure), and then update all callers to handle the
nullable return (or ensure canModify is checked before calling). Reference
symbols: toSerializer(), myPhotolog, EditorNavArgs, canModify.
- Around line 69-79: Refactor the boolean checks to use shared helpers: add a
new computed property isDisplayedPartnerPhotolog (val
isDisplayedPartnerPhotolog: Boolean get() = currentShow == BetweenUs.PARTNER)
and change canModify to use isDisplayedMyPhotolog && isDisplayedGoalCertificated
and canReaction to use isDisplayedPartnerPhotolog && isDisplayedGoalCertificated
(replace direct currentShow == BetweenUs.* comparisons with the new helpers) so
the BetweenUs branching is centralized via isDisplayedMyPhotolog /
isDisplayedPartnerPhotolog.

Comment on lines +151 to +168
private fun uploadPhotolog(fileName: String) {
launchResult(
block = {
photologRepository.uploadPhotoLog(
photologRepository.uploadPhotolog(
PhotologParam(
goalId = goalId,
goalId = navArgs.goalId,
fileName = fileName,
comment = currentState.commentUiModel.comment,
verificationDate = LocalDate.now(),
comment = currentState.comment.value,
verificationDate = LocalDate.parse(navArgs.selectedDate),
),
)
},
onSuccess = { handleUploadPhotologSuccess() },
onError = {
showToast(R.string.task_certification_upload_fail, ToastType.ERROR)
},
)
}
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
# Find launchResult function definition
rg "fun launchResult" --type kotlin -A 20

Repository: YAPP-Github/Twix-Android

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Find the ViewModel base class and its structure
fd "TaskCertificationViewModel.kt" -x cat -n {}

Repository: YAPP-Github/Twix-Android

Length of output: 9039


🏁 Script executed:

#!/bin/bash
# Look for navArgs definition and selectedDate format
rg "selectedDate|navArgs" --type kotlin -B 2 -A 2

Repository: YAPP-Github/Twix-Android

Length of output: 30341


🏁 Script executed:

#!/bin/bash
# Find BaseViewModel implementation
fd "BaseViewModel.kt" -x cat -n {}

Repository: YAPP-Github/Twix-Android

Length of output: 5361


🏁 Script executed:

#!/bin/bash
# Search for launchResult function definition in BaseViewModel
rg "fun launchResult" --type kotlin -B 2 -A 15

Repository: YAPP-Github/Twix-Android

Length of output: 50


LocalDate.parse(navArgs.selectedDate) 파싱 실패에 대한 보호가 필요합니다.

Line 159에서 LocalDate.parse(navArgs.selectedDate)를 호출하는데, launchResult의 예외 처리가 충분하지 않습니다. launchResulttry-catch 블록은 CancellationException만 처리하고, DateTimeParseException과 같은 다른 예외는 처리하지 않아 런타임 크래시로 이어질 수 있습니다.

개선 방안:

  1. navArgs.selectedDate가 비어있거나 잘못된 형식일 경우를 대비한 사전 검증 추가
  2. 파싱을 시도하기 전에 형식 유효성 검사
  3. 예외 처리를 명시적으로 추가하거나, 파싱 로직을 별도 함수로 분리하여 안전하게 처리

예를 들어, block 밖에서 미리 검증하는 방식:

private fun uploadPhotolog(fileName: String) {
    val verificationDate = try {
        LocalDate.parse(navArgs.selectedDate)
    } catch (e: Exception) {
        showToast(R.string.task_certification_upload_fail, ToastType.ERROR)
        return
    }
    
    launchResult(
        block = {
            photologRepository.uploadPhotolog(
                PhotologParam(
                    goalId = navArgs.goalId,
                    fileName = fileName,
                    comment = currentState.comment.value,
                    verificationDate = verificationDate,
                ),
            )
        },
        onSuccess = { handleUploadPhotologSuccess() },
        onError = {
            showToast(R.string.task_certification_upload_fail, ToastType.ERROR)
        },
    )
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt`
around lines 151 - 168, The uploadPhotolog function currently calls
LocalDate.parse(navArgs.selectedDate) inside the launchResult block which can
throw DateTimeParseException and crash; before calling launchResult, validate or
safely parse navArgs.selectedDate (e.g., check for blank and attempt parsing
inside a try/catch) and if parsing fails show the same error toast and return
early; then pass the successfully parsed verificationDate into PhotologParam
when calling photologRepository.uploadPhotolog so uploadPhotolog,
navArgs.selectedDate, LocalDate.parse, PhotologParam, launchResult and
handleUploadPhotologSuccess are updated accordingly to avoid unhandled parsing
exceptions.

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

Labels

Feature Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants