From d83c4b12ec93dd0516a6391b3c0f676dfdea02d9 Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 5 Mar 2026 11:24:01 +0900 Subject: [PATCH] =?UTF-8?q?fix(live):=20=EC=A2=85=EB=A3=8C=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=20=EC=83=81=EB=8C=80=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EC=9D=84=20=EB=A1=9C=EC=BB=AC=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=AD=EC=A0=9C=ED=99=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../live/GetLatestFinishedLiveResponse.kt | 3 +- .../live/LatestFinishedLiveAdapter.kt | 88 ++++++++++++++++++- app/src/main/res/values-en/strings.xml | 4 + app/src/main/res/values-ja/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + docs/20260305_종료라이브상대시간국제화적용.md | 22 +++++ 6 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 docs/20260305_종료라이브상대시간국제화적용.md diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/GetLatestFinishedLiveResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/GetLatestFinishedLiveResponse.kt index e03b3512..928d27c9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/GetLatestFinishedLiveResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/GetLatestFinishedLiveResponse.kt @@ -8,5 +8,6 @@ data class GetLatestFinishedLiveResponse( @SerializedName("memberId") val memberId: Long, @SerializedName("nickname") val nickname: String, @SerializedName("profileImageUrl") val profileImageUrl: String, - @SerializedName("timeAgo") val timeAgo: String + @SerializedName("timeAgo") val timeAgo: String, + @SerializedName("dateUtc") val dateUtc: String ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LatestFinishedLiveAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LatestFinishedLiveAdapter.kt index f2153a13..89dbd279 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LatestFinishedLiveAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LatestFinishedLiveAdapter.kt @@ -5,7 +5,14 @@ import android.content.Context import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions +import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.databinding.ItemLatestFinishedLiveBinding +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone class LatestFinishedLiveAdapter( private val onClick: (Long) -> Unit @@ -49,8 +56,87 @@ class LatestFinishedLiveAdapter( .into(binding.ivProfile) binding.tvNickname.text = item.nickname - binding.tvTimeAgo.text = item.timeAgo + binding.tvTimeAgo.text = relativeTimeText(item) binding.root.setOnClickListener { onClick(item.memberId) } } + + private fun relativeTimeText(item: GetLatestFinishedLiveResponse): String { + val pastMillis = parseDateUtcToMillis(item.dateUtc) + if (pastMillis == null) { + return item.timeAgo + } + + val timezone = TimeZone.getDefault() + val nowCalendar = Calendar.getInstance(timezone, Locale.getDefault()) + val pastCalendar = Calendar.getInstance(timezone, Locale.getDefault()).apply { + timeInMillis = pastMillis + } + + val minute = 60_000L + val hour = 60 * minute + val day = 24 * hour + + val diff = (nowCalendar.timeInMillis - pastCalendar.timeInMillis).coerceAtLeast(0L) + + return when { + diff < minute -> context.getString(R.string.latest_finished_live_time_just_now) + diff < hour -> { + val minutes = (diff / minute).toInt() + context.getString(R.string.latest_finished_live_time_minutes, minutes) + } + + diff < day -> { + val hours = (diff / hour).toInt() + context.getString(R.string.latest_finished_live_time_hours, hours) + } + + else -> { + val days = (diff / day).toInt() + context.getString(R.string.latest_finished_live_time_days, days) + } + } + } + + private fun parseDateUtcToMillis(dateUtc: String?): Long? { + if (dateUtc.isNullOrBlank()) return null + + val value = dateUtc.trim() + if (value.all { it.isDigit() }) { + return try { + val epoch = value.toLong() + if (value.length <= 10) epoch * 1000 else epoch + } catch (exception: NumberFormatException) { + null + } + } + + val patterns = listOf( + "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + "yyyy-MM-dd'T'HH:mm:ssXXX", + "yyyy-MM-dd'T'HH:mm:ss.SSSX", + "yyyy-MM-dd'T'HH:mm:ssX", + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "yyyy-MM-dd'T'HH:mm:ss'Z'", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd HH:mm:ss", + "yyyy/MM/dd HH:mm:ss" + ) + + for (pattern in patterns) { + try { + val dateFormat = SimpleDateFormat(pattern, Locale.US) + dateFormat.timeZone = TimeZone.getTimeZone("UTC") + val parsed: Date? = dateFormat.parse(value) + if (parsed != null) { + return parsed.time + } + } catch (exception: ParseException) { + continue + } + } + + return null + } } } diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index af938ec4..1d4c7bdc 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -272,6 +272,10 @@ %1$d d ago %1$d mo ago %1$d y ago + Just now + %1$d min ago + %1$d h ago + %1$d d ago Enter the details. Enter the reason for reporting. Your report has been submitted. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index b2a80adc..57912526 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -272,6 +272,10 @@ %1$d日前 %1$dヶ月前 %1$d年前 + たった今 + %1$d分前 + %1$d時間前 + %1$d日前 内容を入力してください。 通報理由を入力してください。 通報を受け付けました。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 133e40f7..10d37b8f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -271,6 +271,10 @@ %1$d일전 %1$d개월전 %1$d년전 + 방금 전 + %1$d분 전 + %1$d시간 전 + %1$d일 전 내용을 입력하세요 신고 사유를 입력하세요 신고가 접수되었습니다. diff --git a/docs/20260305_종료라이브상대시간국제화적용.md b/docs/20260305_종료라이브상대시간국제화적용.md new file mode 100644 index 00000000..d8975621 --- /dev/null +++ b/docs/20260305_종료라이브상대시간국제화적용.md @@ -0,0 +1,22 @@ +# 종료 라이브 상대시간 국제화 적용 + +- [x] `GetLatestFinishedLiveResponse`의 `dateUtc`를 기준으로 상대시간 계산 방식 정의 확인 +- [x] `LatestFinishedLiveAdapter`에서 UTC -> 기기 타임존 기준 상대시간 계산 로직 적용 +- [x] `방금 전 / OO분 전 / OO시간 전 / OO일 전` 문자열 국제화 리소스 적용 +- [x] 변경 파일 진단 및 테스트/빌드 검증 수행 + +## 검증 기록 + +### 2026-03-05 +- 무엇을: `LatestFinishedLiveAdapter`에서 `item.timeAgo` 직접 노출 대신 `dateUtc`를 UTC로 파싱한 후 기기 타임존 기준 현재 시각과 비교해 `방금 전 / 분 전 / 시간 전 / 일 전` 형태로 표시하도록 변경. +- 왜: 서버 문자열 의존을 줄이고, 기기 로컬 타임존 기준의 일관된 상대시간 표기 및 다국어 리소스 기반 UI를 적용하기 위해. +- 어떻게: + - `app/src/main/java/kr/co/vividnext/sodalive/live/LatestFinishedLiveAdapter.kt`에 UTC 파싱(`parseDateUtcToMillis`)과 상대시간 계산(`relativeTimeText`) 추가. + - `app/src/main/res/values/strings.xml`, `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml`에 `latest_finished_live_time_*` 문자열 추가. + - LSP 진단 시도: `.kt`, `.xml` 확장자용 LSP 서버 미구성으로 자동 진단 불가 확인. + - 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 결과: `BUILD SUCCESSFUL` (단위 테스트 및 디버그 빌드 성공, 기존 경고만 존재) + - 실행 명령: `./gradlew :app:lintDebug` + - 결과: `:app:lintDebug FAILED` (기존 이슈로 판단되는 `AndroidManifest.xml`의 `com.facebook.FacebookActivity` MissingClass 포함, 총 16 errors/573 warnings) + - 재검증 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 재검증 결과: `BUILD SUCCESSFUL` (어댑터 파싱 패턴 보강 후에도 테스트/빌드 정상)