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` (어댑터 파싱 패턴 보강 후에도 테스트/빌드 정상)