커뮤니티 게시글 상대 시간 표기 다국어 지원

This commit is contained in:
Yu Sung
2025-12-19 14:20:47 +09:00
parent f51fe327e9
commit bea50b0085
5 changed files with 147 additions and 2 deletions

View File

@@ -58,7 +58,7 @@ struct CreatorCommunityAllItemView: View {
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
Text(item.date) Text(item.relativeTimeText())
.font(.custom(Font.light.rawValue, size: 13.3)) .font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color.gray77) .foregroundColor(Color.gray77)
} }
@@ -175,6 +175,7 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider {
content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
price: 10, price: 10,
date: "3일전", date: "3일전",
dateUtc: "2025-08-10T15:00:00",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: true, isLike: true,

View File

@@ -26,7 +26,7 @@ struct CreatorCommunityItemView: View {
.font(.custom(Font.preBold.rawValue, size: 18)) .font(.custom(Font.preBold.rawValue, size: 18))
.foregroundColor(Color.white) .foregroundColor(Color.white)
Text(item.date) Text(item.relativeTimeText())
.font(.custom(Font.preRegular.rawValue, size: 14)) .font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "78909C")) .foregroundColor(Color(hex: "78909C"))
} }
@@ -100,6 +100,7 @@ struct CreatorCommunityItemView_Previews: PreviewProvider {
content: "안녕하세요", content: "안녕하세요",
price: 10, price: 10,
date: "3일전", date: "3일전",
dateUtc: "2025-08-10T15:00:00",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: false, isLike: false,

View File

@@ -5,6 +5,8 @@
// Created by klaus on 2023/12/19. // Created by klaus on 2023/12/19.
// //
import Foundation
struct GetCommunityPostListResponse: Decodable { struct GetCommunityPostListResponse: Decodable {
let postId: Int let postId: Int
let creatorId: Int let creatorId: Int
@@ -15,6 +17,7 @@ struct GetCommunityPostListResponse: Decodable {
let content: String let content: String
let price: Int let price: Int
let date: String let date: String
let dateUtc: String
let isCommentAvailable: Bool let isCommentAvailable: Bool
let isAdult: Bool let isAdult: Bool
let isLike: Bool let isLike: Bool
@@ -23,3 +26,96 @@ struct GetCommunityPostListResponse: Decodable {
let commentCount: Int let commentCount: Int
let firstComment: GetCommunityPostCommentListItem? let firstComment: GetCommunityPostCommentListItem?
} }
// MARK: -
extension GetCommunityPostListResponse {
/// `dateUtc`(UTC )
/// ( ///// ) .
/// , `date` .
func relativeTimeText(now: Date = Date()) -> String {
guard let createdAt = DateParser.parse(dateUtc) else {
return date
}
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))
}
}
}
// 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
}()
}
}

View File

@@ -11,6 +11,51 @@ import Foundation
// String Catalog i18n. // String Catalog i18n.
// LanguageHeaderProvider.current("ko"|"en"|"ja"). // LanguageHeaderProvider.current("ko"|"en"|"ja").
enum I18n { enum I18n {
enum Time {
static var justNow: String {
pick(ko: "방금 전", en: "Just now", ja: "たった今")
}
static func minutesAgo(_ minutes: Int) -> String {
pick(
ko: "\(minutes)분 전",
en: "\(minutes) minute\(minutes == 1 ? "" : "s") ago",
ja: "\(minutes)分前"
)
}
static func hoursAgo(_ hours: Int) -> String {
pick(
ko: "\(hours)시간 전",
en: "\(hours) hour\(hours == 1 ? "" : "s") ago",
ja: "\(hours)時間前"
)
}
static func daysAgo(_ days: Int) -> String {
pick(
ko: "\(days)일 전",
en: "\(days) day\(days == 1 ? "" : "s") ago",
ja: "\(days)日前"
)
}
static func monthsAgo(_ months: Int) -> String {
pick(
ko: "\(months)개월 전",
en: "\(months) month\(months == 1 ? "" : "s") ago",
ja: "\(months)か月前"
)
}
static func yearsAgo(_ years: Int) -> String {
pick(
ko: "\(years)년 전",
en: "\(years) year\(years == 1 ? "" : "s") ago",
ja: "\(years)年前"
)
}
}
enum Common { enum Common {
static var viewAll: String { pick(ko: "전체보기", en: "View all", ja: "すべて見る") } static var viewAll: String { pick(ko: "전체보기", en: "View all", ja: "すべて見る") }

View File

@@ -54,6 +54,7 @@ struct SectionCommunityPostView_Previews: PreviewProvider {
content: "라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", content: "라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
price: 10, price: 10,
date: "3일전", date: "3일전",
dateUtc: "2025-08-10T15:00:00",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: true, isLike: true,
@@ -72,6 +73,7 @@ struct SectionCommunityPostView_Previews: PreviewProvider {
content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
price: 10, price: 10,
date: "3일전", date: "3일전",
dateUtc: "2025-08-10T15:00:00",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: true, isLike: true,