Skip to content

[FIX/#1591] 앱잼탬프 QA 사항 반영#1598

Open
seungjunGong wants to merge 4 commits into
developfrom
FIX/#1591-appjamtamp-qa-clean
Open

[FIX/#1591] 앱잼탬프 QA 사항 반영#1598
seungjunGong wants to merge 4 commits into
developfrom
FIX/#1591-appjamtamp-qa-clean

Conversation

@seungjunGong
Copy link
Copy Markdown
Member

Related issue 🛠

Work Description ✏️

  • 앱잼 현황 최근 스토리 카드의 업로드 시간 표기 로직을 수정
    • 10분 미만은 방금 전
    • 10분~59분은 n분 전
    • 1시간~24시간대는 n시간 전
    • 이후는 n일 전, n주 전, n월n일 형식으로 노출되도록 반영
  • 미션 작성/수정 화면 완료 버튼 활성화 조건 개선
    • 작성: 필수값 충족 여부 체크
    • 수정: 필수값 충족 + 변경 여부 체크
  • 미션 제출 완료 후 배지 애니메이션 종료 시 상세 화면에서 자동으로 이전 화면으로 복귀하도록 처리
  • 뒤로가기 헤더 버튼 클릭을 throttledNoRippleClickable로 변경해 중복 클릭 방지

Screenshot 📸

  • N/A 자체 QA 확인

Uncompleted Tasks 😅

  • N/A

To Reviewers 📢

스토리 카드 시간 표기 기준은 조금 많이 변경되었는데 지난 QA 때 의견이 나와서 4주 이후에는 n월 n일로 수정하기로 해서 변경했습니다..!

@seungjunGong seungjunGong added this to the 38th Android milestone May 30, 2026
@seungjunGong seungjunGong self-assigned this May 30, 2026
@seungjunGong seungjunGong requested a review from a team as a code owner May 30, 2026 07:34
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Review Change Stack

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 미션 완료 버튼의 활성화/비활성화 상태 관리 기능 추가
  • 버그 수정

    • 버튼 비활성화 상태에서 시각적 피드백 개선
    • 상대 시간 표시 로직 개선 (방금 전, 분/시간/일/주 단위 표시)
    • 뒤로가기 버튼 빠른 연속 클릭 방지

둘러보기

이 PR은 앱잼 탬프 QA 피드백을 반영하여 버튼 상태 제어, 뒤로가기 연속 클릭 방지, 미션 수정 시 변경 감지 기반 버튼 활성화, 그리고 상대 시간 표시 정확도를 개선합니다.

변경사항

앱잼 탬프 QA 개선사항

레이어 / 파일 요약
버튼 상태 제어 및 클릭 처리 개선
feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/component/AppjamtampButton.kt, BackButtonHeader.kt
AppjamtampButton에 isEnabled 파라미터를 추가하여 배경색을 조건에 따라 분기(활성: primary, 비활성: onSurface300)하고, noRippleClickable로 클릭을 제어하며, BackButtonHeader의 뒤로가기 버튼에 throttledNoRippleClickable을 적용하여 연속 클릭을 방지합니다.
미션 상태 모델 및 초기 스냅샷 관리
feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailState.kt
MissionDetailState에 초기 스냅샷 필드(initSnapshotImageModel, initSnapshotDate, initSnapshotContent)를 추가하고, isSubmitEnabled 계산 프로퍼티를 구현하여 로딩 상태, 필드 유효성, viewType(WRITE/EDIT/READ_ONLY/COMPLETE), 그리고 EDIT 시 초기값과의 변경 여부에 기반하여 제출 버튼 활성화를 결정합니다.
뷰모델 스냅샷 초기화 및 업데이트
feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailViewModel.kt
미션 조회 성공 시 initSnapshotImageModel, initSnapshotDate, initSnapshotContent를 서버 데이터로 초기화하고, 미션 수정 성공 후 viewType을 COMPLETE로 변경한 뒤 수정된 값으로 스냅샷을 갱신하여 다음 편집 시 변경 감지를 지원합니다.
라우트 통합 및 화면 연결
feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailRoute.kt
uiState.isSubmitEnabled를 MyEmptyMissionDetailScreen과 MissionDetailScreen에 전달하여 "미션 완료" 버튼의 활성화 상태를 제어하고, showPostSubmissionBadge 조건을 추가하여 배지 업데이트 후 네비게이션을 수행하며, 프리뷰 함수를 함께 업데이트합니다.
상대 시간 표시 개선
feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/Top3RecentRankingMission.kt
toRelativeTime() 함수를 개선하여 runCatching으로 예외 처리를 안전하게 하고, 시간 차이를 더욱 세밀하게 분류(방금 전, 분 전, 시간 전, 일 전, 주 전, M월d일 형식)하여 정확한 상대 시간을 표시합니다.

예상 코드 리뷰 난이도

🎯 3 (보통) | ⏱️ ~25분

추천 리뷰어

  • 1971123-seongmin
  • vvan2

🐰 변경 축하 래빗의 시

탬프에 손이 살짝, 한 번만 눌러도
버튼은 똑똑이 활성화 판단해
뒤로가기는 쏜살같이 한 번에만,
초기값과의 비교로 변경을 감지하고
상대 시간도 더 정확해졌네! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive 이슈 #1591에 명시되지 않은 '최근 스토리 카드 업로드 시간 표기 로직 수정'이 PR 설명과 코드 변경[Top3RecentRankingMission.kt]에서 관찰되며, PR 설명에서 이 변경에 대한 추가 배경을 언급하고 있다. 이슈 #1591에 포함되지 않은 시간 표기 로직 변경이 별도 요구사항인지 또는 같은 QA 범위에 포함되어야 하는지 명확히 해야 한다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 '#1591' 이슈를 명확히 참조하며 '앱잼탬프 QA 사항 반영'으로 변경 사항을 요약하고 있으나, 구체적인 주요 변경 내용(시간 표기 로직, 버튼 활성화 조건, 자동 복귀 등)을 직관적으로 파악하기 어렵다.
Description check ✅ Passed PR 설명은 이슈 #1591을 참조하고 구체적인 작업 내용(시간 표기 로직 수정, 버튼 활성화 조건, 자동 복귀, 중복 클릭 방지)을 명확히 기술하고 있으며, 변경 사항과 직접 관련되어 있다.
Linked Issues check ✅ Passed PR이 구현한 모든 코드 변경 사항이 이슈 #1591의 요구 사항들과 일치한다: (1) throttledNoRippleClickable로 중복 클릭 방지 [BackButtonHeader.kt], (2) 스탬프 애니메이션 후 자동 복귀 구현 [MissionDetailRoute.kt], (3) 미션 수정 시 변경 여부 체크로 버튼 비활성화 [MissionDetailState.kt, MissionDetailViewModel.kt].

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch FIX/#1591-appjamtamp-qa-clean

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

❤️ Share

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

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces submission state tracking and validation to the mission detail screen, replaces standard click modifiers with ripple-free and throttled alternatives, and refines the relative time formatting logic. Feedback focuses on ensuring that initial snapshots are updated upon successful mission submission, caching SimpleDateFormat instances to prevent recomposition overhead, optimizing LaunchedEffect keys to avoid redundant coroutine restarts, and conditionally applying click modifiers on the button component to improve accessibility when disabled.

Comment on lines +267 to +269
initSnapshotImageModel = ImageModel.Remote(imageModel.url),
initSnapshotDate = date,
initSnapshotContent = content,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

modifyMission()에서는 초기 스냅샷 값(initSnapshotImageModel, initSnapshotDate, initSnapshotContent)을 올바르게 업데이트하고 있지만, 새로운 미션을 작성하는 submitMission()에서는 성공적으로 제출된 후 이 스냅샷 값들을 업데이트하지 않고 있습니다.

이로 인해 사용자가 새 미션을 제출한 직후 상세 화면에서 바로 수정을 시도할 경우, 초기 스냅샷 값이 여전히 비어 있는 상태(기본값)로 유지되어 isSubmitEnabled 조건이 오동작하게 됩니다(예: 제출된 미션과 비교해 아무런 변경 사항이 없음에도 완료 버튼이 활성화됨).

submitMission()onSuccess 블록에서도 이 스냅샷 값들을 최신 상태로 업데이트하도록 보완해 주세요.

Comment on lines 158 to 182
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.KOREA)
dateFormat.timeZone = TimeZone.getTimeZone("Asia/Seoul")
val monthDayFormat = SimpleDateFormat("M월d일", Locale.KOREA).apply {
timeZone = TimeZone.getTimeZone("Asia/Seoul")
}

val date = dateFormat.parse(this) ?: return ""
val date = runCatching { dateFormat.parse(this) }.getOrNull() ?: return ""
val currentDate = Date()

val diffMillis = currentDate.time - date.time
if (diffMillis < 0) return "1분 전"
if (diffMillis < 0) return "방금 전"

val minutes = TimeUnit.MILLISECONDS.toMinutes(diffMillis)
val hours = TimeUnit.MILLISECONDS.toHours(diffMillis)
val days = TimeUnit.MILLISECONDS.toDays(diffMillis)

return when {
minutes == 0L -> "1분 전"
minutes in 1..59 -> "${minutes}분 전"
hours in 1..24 -> "${hours}시간 전"
else -> {
val days = TimeUnit.MILLISECONDS.toDays(diffMillis)
"${days}일 전"
}
minutes < 10L -> "방금 전"
minutes < 60L -> "${minutes}분 전"
hours < 25L -> "${hours}시간 전"
days < 7L -> "${days}일 전"
days < 35L -> "${days / 7}주 전"
else -> monthDayFormat.format(date)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

toRelativeTime() 함수가 호출될 때마다 SimpleDateFormat 인스턴스를 매번 생성하는 것은 비효율적입니다. 특히 이 함수는 리스트 아이템의 Jetpack Compose 리컴포지션 중에 호출되므로, SimpleDateFormat 인스턴스 생성 비용이 누적되어 성능 저하를 유발할 수 있습니다.

성능을 최적화하고 불필요한 객체 할당을 피하기 위해 포맷터를 캐싱하는 것이 좋습니다. SimpleDateFormat은 스레드 안전하지 않으므로, 파일 수준의 private Holder 객체 내부에 ThreadLocal을 사용하여 안전하게 캐싱할 수 있습니다.

    val date = runCatching { Holder.dateFormat.get().parse(this) }.getOrNull() ?: return ""
    val currentDate = Date()

    val diffMillis = currentDate.time - date.time
    if (diffMillis < 0) return "방금 전"

    val minutes = TimeUnit.MILLISECONDS.toMinutes(diffMillis)
    val hours = TimeUnit.MILLISECONDS.toHours(diffMillis)
    val days = TimeUnit.MILLISECONDS.toDays(diffMillis)

    return when {
        minutes < 10L -> "방금 전"
        minutes < 60L -> "${minutes}분 전"
        hours < 25L -> "${hours}시간 전"
        days < 7L -> "${days}일 전"
        days < 35L -> "${days / 7}주 전"
        else -> Holder.monthDayFormat.get().format(date)
    }
}

private object Holder {
    val dateFormat = object : ThreadLocal<SimpleDateFormat>() {
        override fun initialValue() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.KOREA).apply {
            timeZone = TimeZone.getTimeZone("Asia/Seoul")
        }
    }
    val monthDayFormat = object : ThreadLocal<SimpleDateFormat>() {
        override fun initialValue() = SimpleDateFormat("M월d일", Locale.KOREA).apply {
            timeZone = TimeZone.getTimeZone("Asia/Seoul")
        }
    }
}

Comment on lines +133 to 139
LaunchedEffect(showPostSubmissionBadge, !uiState.isLoading, progress) {
if (showPostSubmissionBadge && progress >= 0.99f && !uiState.isLoading) {
delay(500L)
viewModel.updateShowPostSubmissionBadge()
navigateUp()
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

progressLaunchedEffect의 키로 사용하면, progress >= 0.99f 조건이 참이 된 이후에도 Lottie 애니메이션의 매 프레임마다(즉, progress 값이 미세하게 변경될 때마다) 이펙트가 취소되고 재시작됩니다. 이로 인해 불필요한 코루틴 취소 및 재시작이 발생하며, 애니메이션이 완전히 멈출 때까지 delay(500L)가 계속 뒤로 밀리게 됩니다.

대신 LaunchedEffect의 키로는 showPostSubmissionBadge만 사용하고, 내부에서 snapshotFlow를 통해 progress와 loading 조건을 대기하도록 변경하면 불필요한 재시작 없이 부드럽게 동작합니다.

참고: androidx.compose.runtime.snapshotFlowkotlinx.coroutines.flow.first 임포트가 필요합니다.

    LaunchedEffect(showPostSubmissionBadge) {
        if (showPostSubmissionBadge) {
            snapshotFlow { progress >= 0.99f && !uiState.isLoading }
                .first { it }
            delay(500L)
            viewModel.updateShowPostSubmissionBadge()
            navigateUp()
        }
    }

Comment on lines 50 to +54
.background(
color = SoptTheme.colors.primary,
color = if (isEnabled) SoptTheme.colors.primary else SoptTheme.colors.onSurface300,
shape = RoundedCornerShape(9.dp),
)
.clickable(onClick = onClicked),
.noRippleClickable(onClick = { if (isEnabled) onClicked() }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

isEnabled가 false일 때 버튼은 시각적으로 비활성화되지만, 클릭 한정자(noRippleClickable)는 여전히 연결되어 있어 클릭 이벤트를 가로챕니다(람다 내부에서 아무 동작도 하지 않더라도). 이는 TalkBack과 같은 접근성 서비스나 키보드 탐색 시 비활성화된 버튼이 여전히 클릭 가능한 것처럼 인식되는 문제를 유발할 수 있습니다.

버튼이 비활성화되었을 때는 클릭 리스너 자체가 연결되지 않도록 .let을 사용하여 noRippleClickable 한정자를 조건부로 적용하는 것이 좋습니다.

            .background(
                color = if (isEnabled) SoptTheme.colors.primary else SoptTheme.colors.onSurface300,
                shape = RoundedCornerShape(9.dp),
            )
            .let {
                if (isEnabled) it.noRippleClickable(onClick = onClicked) else it
            }

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/Top3RecentRankingMission.kt`:
- Around line 179-180: The week-based formatting branch in
Top3RecentRankingMission (the days-based when/else that currently uses `days <
35L`) incorrectly includes 28–34 days as "n주 전"; change the condition to limit
week display to 27 days (e.g., `days < 28L` or `days <= 27L`) so that any date
>= 28 days falls through to the else branch and uses monthDayFormat.format(date)
per the spec.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a13e6071-d273-44d7-91a9-60047a92a5e3

📥 Commits

Reviewing files that changed from the base of the PR and between dd567db and 9e3753e.

📒 Files selected for processing (6)
  • feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/component/AppjamtampButton.kt
  • feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/component/BackButtonHeader.kt
  • feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailRoute.kt
  • feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailState.kt
  • feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailViewModel.kt
  • feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/Top3RecentRankingMission.kt

Comment on lines +179 to +180
days < 35L -> "${days / 7}주 전"
else -> monthDayFormat.format(date)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

4주 이후 포맷 요구사항과 분기 조건이 어긋납니다.

Line 179의 days < 35L 조건 때문에 28~34일이 "4주 전"으로 표시됩니다. PR 명세(4주 이후는 "n월 n일")에 맞추려면 주 단위 표기는 27일까지로 제한되어야 합니다.

수정 제안 diff
-        days < 35L -> "${days / 7}주 전"
+        days < 28L -> "${days / 7}주 전"
         else -> monthDayFormat.format(date)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/Top3RecentRankingMission.kt`
around lines 179 - 180, The week-based formatting branch in
Top3RecentRankingMission (the days-based when/else that currently uses `days <
35L`) incorrectly includes 28–34 days as "n주 전"; change the condition to limit
week display to 27 days (e.g., `days < 28L` or `days <= 27L`) so that any date
>= 28 days falls through to the else branch and uses monthDayFormat.format(date)
per the spec.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FIX] 앱잼 탬프 QA 수정 사항 반영

1 participant