완료 방송 상대 시간 표시
최근 종료 방송 카드에 UTC 기준 상대 시간 문자열을 표시한다.
This commit is contained in:
60
SodaLive/Sources/Common/DateParser.swift
Normal file
60
SodaLive/Sources/Common/DateParser.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// DateParser.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 12/19/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - 내부: 다양한 포맷 파서를 시도
|
||||
enum DateParser {
|
||||
static func parse(_ text: String) -> Date? {
|
||||
for parser in parsers {
|
||||
if let d = parser(text) { return d }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 시도 순서: ISO8601(소수초 포함) → ISO8601 → RFC3339 유사 → 공백 구분 기본
|
||||
private static let parsers: [(String) -> Date?] = [
|
||||
{ ISO8601.fractional.date(from: $0) },
|
||||
{ ISO8601.basic.date(from: $0) },
|
||||
{ DF.rfc3339.date(from: $0) },
|
||||
{ DF.basic.date(from: $0) }
|
||||
]
|
||||
|
||||
private enum ISO8601 {
|
||||
static let fractional: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
return f
|
||||
}()
|
||||
|
||||
static let basic: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime]
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
return f
|
||||
}()
|
||||
}
|
||||
|
||||
private enum DF {
|
||||
static let rfc3339: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.locale = Locale(identifier: "en_US_POSIX")
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
||||
return f
|
||||
}()
|
||||
|
||||
static let basic: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.locale = Locale(identifier: "en_US_POSIX")
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return f
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -67,55 +67,3 @@ extension GetCommunityPostListResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 내부: 다양한 포맷 파서를 시도
|
||||
private enum DateParser {
|
||||
static func parse(_ text: String) -> Date? {
|
||||
for parser in parsers {
|
||||
if let d = parser(text) { return d }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 시도 순서: ISO8601(소수초 포함) → ISO8601 → RFC3339 유사 → 공백 구분 기본
|
||||
private static let parsers: [(String) -> Date?] = [
|
||||
{ ISO8601.fractional.date(from: $0) },
|
||||
{ ISO8601.basic.date(from: $0) },
|
||||
{ DF.rfc3339.date(from: $0) },
|
||||
{ DF.basic.date(from: $0) }
|
||||
]
|
||||
|
||||
private enum ISO8601 {
|
||||
static let fractional: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
return f
|
||||
}()
|
||||
|
||||
static let basic: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime]
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
return f
|
||||
}()
|
||||
}
|
||||
|
||||
private enum DF {
|
||||
static let rfc3339: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.locale = Locale(identifier: "en_US_POSIX")
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
||||
return f
|
||||
}()
|
||||
|
||||
static let basic: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.locale = Locale(identifier: "en_US_POSIX")
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return f
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,53 @@
|
||||
// Created by klaus on 7/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetLatestFinishedLiveResponse: Decodable {
|
||||
let memberId: Int
|
||||
let nickname: String
|
||||
let profileImageUrl: String
|
||||
let timeAgo: String
|
||||
let dateUtc: String
|
||||
}
|
||||
|
||||
// MARK: - 상대 시간 문자열
|
||||
extension GetLatestFinishedLiveResponse {
|
||||
/// `dateUtc`(UTC 문자열)을 파싱해 디바이스 타임존 기준
|
||||
/// 상대 시간(방금 전/분/시간/일/개월/년 전)을 반환합니다.
|
||||
/// 파싱 실패 시, `date`를 그대로 반환합니다.
|
||||
func relativeTimeText(now: Date = Date()) -> String {
|
||||
guard let createdAt = DateParser.parse(dateUtc) else {
|
||||
return timeAgo
|
||||
}
|
||||
|
||||
let nowDate = now
|
||||
let interval = max(0, nowDate.timeIntervalSince(createdAt))
|
||||
|
||||
// 연/월 차이는 달력 기준으로 계산(윤년/월 길이 반영)
|
||||
let calendar = Calendar.current
|
||||
let ym = calendar.dateComponents([.year, .month],
|
||||
from: createdAt,
|
||||
to: nowDate)
|
||||
if let years = ym.year, years >= 1 {
|
||||
return I18n.Time.yearsAgo(years)
|
||||
}
|
||||
if let months = ym.month, months >= 1 {
|
||||
return I18n.Time.monthsAgo(months)
|
||||
}
|
||||
|
||||
// 일/시간/분은 초 기반으로 간단 처리
|
||||
if interval < 60 {
|
||||
return I18n.Time.justNow
|
||||
} else if interval < 3600 {
|
||||
let minutes = Int(interval / 60)
|
||||
return I18n.Time.minutesAgo(max(1, minutes))
|
||||
} else if interval < 86_400 {
|
||||
let hours = Int(interval / 3600)
|
||||
return I18n.Time.hoursAgo(max(1, hours))
|
||||
} else {
|
||||
let days = Int(interval / 86_400)
|
||||
return I18n.Time.daysAgo(max(1, days))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ struct LatestFinishedLiveItemView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(item.timeAgo)
|
||||
Text(item.relativeTimeText())
|
||||
.font(.custom(Font.preRegular.rawValue, size: 16))
|
||||
.foregroundColor(Color(hex: "78909C"))
|
||||
}
|
||||
@@ -45,7 +45,8 @@ struct LatestFinishedLiveItemView: View {
|
||||
memberId: 1,
|
||||
nickname: "크리에이터 1",
|
||||
profileImageUrl: "https://cf.sodalive.net/profile/34638/34638-profile-5bfc2bac-3278-48f8-b60c-1294b615f629-8832-1751707083877",
|
||||
timeAgo: "5분전"
|
||||
timeAgo: "5분전",
|
||||
dateUtc: "2025-08-10T15:00:00"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,13 +51,15 @@ struct SectionLatestFinishedLiveView: View {
|
||||
memberId: 1,
|
||||
nickname: "크리에이터 1",
|
||||
profileImageUrl: "https://cf.sodalive.net/profile/34638/34638-profile-5bfc2bac-3278-48f8-b60c-1294b615f629-8832-1751707083877",
|
||||
timeAgo: "5분전"
|
||||
timeAgo: "5분전",
|
||||
dateUtc: "2025-08-10T15:00:00"
|
||||
),
|
||||
GetLatestFinishedLiveResponse(
|
||||
memberId: 2,
|
||||
nickname: "크리에이터 2",
|
||||
profileImageUrl: "https://cf.sodalive.net/profile/34638/34638-profile-5bfc2bac-3278-48f8-b60c-1294b615f629-8832-1751707083877",
|
||||
timeAgo: "1시간전"
|
||||
timeAgo: "1시간전",
|
||||
dateUtc: "2025-08-10T15:00:00"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user