From dd51a3fc2e979ce30a01090d16eacaafe3a9e72e Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 19 Dec 2025 23:52:43 +0900 Subject: [PATCH] =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=B0=A9=EC=86=A1=20?= =?UTF-8?q?=EC=83=81=EB=8C=80=20=EC=8B=9C=EA=B0=84=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 최근 종료 방송 카드에 UTC 기준 상대 시간 문자열을 표시한다. --- SodaLive/Sources/Common/DateParser.swift | 60 +++++++++++++++++++ .../GetCommunityPostListResponse.swift | 52 ---------------- .../Live/GetLatestFinishedLiveResponse.swift | 44 ++++++++++++++ .../Live/LatestFinishedLiveItemView.swift | 5 +- .../Live/SectionLatestFinishedLiveView.swift | 6 +- 5 files changed, 111 insertions(+), 56 deletions(-) create mode 100644 SodaLive/Sources/Common/DateParser.swift diff --git a/SodaLive/Sources/Common/DateParser.swift b/SodaLive/Sources/Common/DateParser.swift new file mode 100644 index 0000000..13d17ae --- /dev/null +++ b/SodaLive/Sources/Common/DateParser.swift @@ -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 + }() + } +} diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift index 0f2942a..2ec4164 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift @@ -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 - }() - } -} diff --git a/SodaLive/Sources/Live/GetLatestFinishedLiveResponse.swift b/SodaLive/Sources/Live/GetLatestFinishedLiveResponse.swift index 99a81cb..c00c788 100644 --- a/SodaLive/Sources/Live/GetLatestFinishedLiveResponse.swift +++ b/SodaLive/Sources/Live/GetLatestFinishedLiveResponse.swift @@ -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)) + } + } } diff --git a/SodaLive/Sources/Live/LatestFinishedLiveItemView.swift b/SodaLive/Sources/Live/LatestFinishedLiveItemView.swift index bae569c..5c12a3e 100644 --- a/SodaLive/Sources/Live/LatestFinishedLiveItemView.swift +++ b/SodaLive/Sources/Live/LatestFinishedLiveItemView.swift @@ -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" ) ) } diff --git a/SodaLive/Sources/Live/SectionLatestFinishedLiveView.swift b/SodaLive/Sources/Live/SectionLatestFinishedLiveView.swift index 03002d3..18c67f4 100644 --- a/SodaLive/Sources/Live/SectionLatestFinishedLiveView.swift +++ b/SodaLive/Sources/Live/SectionLatestFinishedLiveView.swift @@ -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" ) ] )