From bea50b0085f35540781b3f580579b56214384c5d Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 19 Dec 2025 14:20:47 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=83=81=EB=8C=80=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=ED=91=9C=EA=B8=B0=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../All/CreatorCommunityAllItemView.swift | 3 +- .../CreatorCommunityItemView.swift | 3 +- .../GetCommunityPostListResponse.swift | 96 +++++++++++++++++++ SodaLive/Sources/I18n/I18n.swift | 45 +++++++++ .../Live/SectionCommunityPostView.swift | 2 + 5 files changed, 147 insertions(+), 2 deletions(-) diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift index 5d65ac9..9effcac 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift @@ -58,7 +58,7 @@ struct CreatorCommunityAllItemView: View { .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color.grayee) - Text(item.date) + Text(item.relativeTimeText()) .font(.custom(Font.light.rawValue, size: 13.3)) .foregroundColor(Color.gray77) } @@ -175,6 +175,7 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider { content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", price: 10, date: "3일전", + dateUtc: "2025-08-10T15:00:00", isCommentAvailable: false, isAdult: false, isLike: true, diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift index aaef21a..8abc106 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift @@ -26,7 +26,7 @@ struct CreatorCommunityItemView: View { .font(.custom(Font.preBold.rawValue, size: 18)) .foregroundColor(Color.white) - Text(item.date) + Text(item.relativeTimeText()) .font(.custom(Font.preRegular.rawValue, size: 14)) .foregroundColor(Color(hex: "78909C")) } @@ -100,6 +100,7 @@ struct CreatorCommunityItemView_Previews: PreviewProvider { content: "안녕하세요", price: 10, date: "3일전", + dateUtc: "2025-08-10T15:00:00", isCommentAvailable: false, isAdult: false, isLike: false, diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift index 6443ac7..0f2942a 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/GetCommunityPostListResponse.swift @@ -5,6 +5,8 @@ // Created by klaus on 2023/12/19. // +import Foundation + struct GetCommunityPostListResponse: Decodable { let postId: Int let creatorId: Int @@ -15,6 +17,7 @@ struct GetCommunityPostListResponse: Decodable { let content: String let price: Int let date: String + let dateUtc: String let isCommentAvailable: Bool let isAdult: Bool let isLike: Bool @@ -23,3 +26,96 @@ struct GetCommunityPostListResponse: Decodable { let commentCount: Int 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 + }() + } +} diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index fd394d6..3b2440a 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -11,6 +11,51 @@ import Foundation // String Catalog를 사용하지 않는 컨텍스트에서 사용할 하드코딩 맵 기반 i18n. // 기준 언어 선택은 LanguageHeaderProvider.current("ko"|"en"|"ja"). 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 { static var viewAll: String { pick(ko: "전체보기", en: "View all", ja: "すべて見る") } diff --git a/SodaLive/Sources/Live/SectionCommunityPostView.swift b/SodaLive/Sources/Live/SectionCommunityPostView.swift index 8d36e40..70e633e 100644 --- a/SodaLive/Sources/Live/SectionCommunityPostView.swift +++ b/SodaLive/Sources/Live/SectionCommunityPostView.swift @@ -54,6 +54,7 @@ struct SectionCommunityPostView_Previews: PreviewProvider { content: "라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", price: 10, date: "3일전", + dateUtc: "2025-08-10T15:00:00", isCommentAvailable: false, isAdult: false, isLike: true, @@ -72,6 +73,7 @@ struct SectionCommunityPostView_Previews: PreviewProvider { content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", price: 10, date: "3일전", + dateUtc: "2025-08-10T15:00:00", isCommentAvailable: false, isAdult: false, isLike: true,