diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 4c3b91fb..4054ae7d 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -1853,6 +1853,7 @@ C0A74100000000000000000F /* UserCreatorChatRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A74000000000000000000F /* UserCreatorChatRoomViewModel.swift */; }; C0A741000000000000000010 /* UserCreatorChatRoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000010 /* UserCreatorChatRoomView.swift */; }; C0A741000000000000000011 /* UserCreatorChatSessionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000011 /* UserCreatorChatSessionNotification.swift */; }; + C0A741000000000000000012 /* UserCreatorChatDatePresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000012 /* UserCreatorChatDatePresentation.swift */; }; C0A742000000000000000001 /* MainChatRoomsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000001 /* MainChatRoomsResponse.swift */; }; C0A742000000000000000002 /* MainChatApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000002 /* MainChatApi.swift */; }; C0A742000000000000000003 /* MainChatRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000003 /* MainChatRepository.swift */; }; @@ -1877,6 +1878,7 @@ C0A74300000000000000000F /* UserCreatorChatRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A74000000000000000000F /* UserCreatorChatRoomViewModel.swift */; }; C0A743000000000000000010 /* UserCreatorChatRoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000010 /* UserCreatorChatRoomView.swift */; }; C0A743000000000000000011 /* UserCreatorChatSessionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000011 /* UserCreatorChatSessionNotification.swift */; }; + C0A743000000000000000012 /* UserCreatorChatDatePresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A740000000000000000012 /* UserCreatorChatDatePresentation.swift */; }; C0D6E325AEFEEF476E7044AB /* CommunityPostCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82912F2F111593BE42E33C /* CommunityPostCard.swift */; }; C3CFF5936F1BAC5BC12E9DDB /* UpdateContentPreferenceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDFAA64C2CA4FCBBF00A024 /* UpdateContentPreferenceRequest.swift */; }; C4A7064704896D68ACBA135F /* PushNotificationListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA7A276BF4A47206ED702A7 /* PushNotificationListItemView.swift */; }; @@ -3097,6 +3099,7 @@ C0A74000000000000000000F /* UserCreatorChatRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCreatorChatRoomViewModel.swift; sourceTree = ""; }; C0A740000000000000000010 /* UserCreatorChatRoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCreatorChatRoomView.swift; sourceTree = ""; }; C0A740000000000000000011 /* UserCreatorChatSessionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCreatorChatSessionNotification.swift; sourceTree = ""; }; + C0A740000000000000000012 /* UserCreatorChatDatePresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCreatorChatDatePresentation.swift; sourceTree = ""; }; C1165E3F4CA1AB019FC85067 /* LanguageRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageRepository.swift; sourceTree = ""; }; C40163E3E9DB325EA984D3C4 /* CreatorChannelFloatingIconButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFloatingIconButton.swift; sourceTree = ""; }; C6B18C00DE256DCC53030630 /* Spacing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Spacing.swift; sourceTree = ""; }; @@ -6165,6 +6168,7 @@ children = ( C0A740000000000000000008 /* UserCreatorChatModels.swift */, C0A740000000000000000009 /* UserCreatorChatDisplayMessage.swift */, + C0A740000000000000000012 /* UserCreatorChatDatePresentation.swift */, ); path = Models; sourceTree = ""; @@ -7147,6 +7151,7 @@ C0A741000000000000000007 /* MainChatFilter.swift in Sources */, C0A741000000000000000008 /* UserCreatorChatModels.swift in Sources */, C0A741000000000000000009 /* UserCreatorChatDisplayMessage.swift in Sources */, + C0A741000000000000000012 /* UserCreatorChatDatePresentation.swift in Sources */, C0A74100000000000000000A /* UserCreatorChatApi.swift in Sources */, C0A74100000000000000000B /* UserCreatorChatRepository.swift in Sources */, C0A74100000000000000000C /* UserCreatorChatWebSocketModels.swift in Sources */, @@ -9031,6 +9036,7 @@ C0A743000000000000000007 /* MainChatFilter.swift in Sources */, C0A743000000000000000008 /* UserCreatorChatModels.swift in Sources */, C0A743000000000000000009 /* UserCreatorChatDisplayMessage.swift in Sources */, + C0A743000000000000000012 /* UserCreatorChatDatePresentation.swift in Sources */, C0A74300000000000000000A /* UserCreatorChatApi.swift in Sources */, C0A74300000000000000000B /* UserCreatorChatRepository.swift in Sources */, C0A74300000000000000000C /* UserCreatorChatWebSocketModels.swift in Sources */, diff --git a/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDatePresentation.swift b/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDatePresentation.swift new file mode 100644 index 00000000..4cda499a --- /dev/null +++ b/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDatePresentation.swift @@ -0,0 +1,16 @@ +import Foundation + +enum UserCreatorChatDatePresentation { + static func title(for date: Date, locale: Locale, calendar: Calendar, timeZone: TimeZone) -> String { + var calendar = calendar + calendar.timeZone = timeZone + + let formatter = DateFormatter() + formatter.locale = locale + formatter.timeZone = timeZone + formatter.calendar = calendar + formatter.doesRelativeDateFormatting = false + formatter.setLocalizedDateFormatFromTemplate("yMMMMdEEEE") + return formatter.string(from: date) + } +} diff --git a/work/tests/UserCreatorChatDatePresentationCheck.swift b/work/tests/UserCreatorChatDatePresentationCheck.swift new file mode 100644 index 00000000..e1a41888 --- /dev/null +++ b/work/tests/UserCreatorChatDatePresentationCheck.swift @@ -0,0 +1,88 @@ +import Foundation + +@main +enum UserCreatorChatDatePresentationCheck { + static var assertions = 0 + + static func check(_ condition: Bool, _ message: String) { + guard condition else { print("FAIL: \(message)"); exit(1) } + assertions += 1 + } + + static func checkTitles(for date: Date, calendar: Calendar, timeZone: TimeZone, expected: [String], label: String) { + for (localeID, expectedTitle) in zip(["ko_KR", "en_US", "ja_JP"], expected) { + let title = UserCreatorChatDatePresentation.title( + for: date, + locale: Locale(identifier: localeID), + calendar: calendar, + timeZone: timeZone + ) + check(title == expectedTitle, "\(label) \(localeID): expected '\(expectedTitle)', got '\(title)'") + } + } + + static func main() { + let utc = TimeZone(secondsFromGMT: 0)! + let seoul = TimeZone(identifier: "Asia/Seoul")! + let losAngeles = TimeZone(identifier: "America/Los_Angeles")! + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = utc + var seoulCalendar = calendar + seoulCalendar.timeZone = seoul + var losAngelesCalendar = calendar + losAngelesCalendar.timeZone = losAngeles + + let fixtures: [(label: String, timestamp: String, calendar: Calendar, timeZone: TimeZone, expected: [String])] = [ + ("Gregorian UTC fixture", "2026-10-16T12:00:00Z", calendar, utc, + ["2026년 10월 16일 금요일", "Friday, October 16, 2026", "2026年10月16日 金曜日"]), + ("previous year", "2025-10-16T12:00:00Z", calendar, utc, + ["2025년 10월 16일 목요일", "Thursday, October 16, 2025", "2025年10月16日 木曜日"]), + ("UTC boundary", "2026-10-16T15:30:00Z", calendar, utc, + ["2026년 10월 16일 금요일", "Friday, October 16, 2026", "2026年10月16日 金曜日"]), + ("Seoul boundary in every app locale", "2026-10-16T15:30:00Z", seoulCalendar, seoul, + ["2026년 10월 17일 토요일", "Saturday, October 17, 2026", "2026年10月17日 土曜日"]), + ("Los Angeles boundary in every app locale", "2026-10-16T15:30:00Z", losAngelesCalendar, losAngeles, + ["2026년 10월 16일 금요일", "Friday, October 16, 2026", "2026年10月16日 金曜日"]), + ("Los Angeles calendar normalized to Seoul", "2026-10-16T15:30:00Z", losAngelesCalendar, seoul, + ["2026년 10월 17일 토요일", "Saturday, October 17, 2026", "2026年10月17日 土曜日"]), + ("Seoul calendar normalized to Los Angeles", "2026-10-16T15:30:00Z", seoulCalendar, losAngeles, + ["2026년 10월 16일 금요일", "Friday, October 16, 2026", "2026年10月16日 金曜日"]) + ] + let isoFormatter = ISO8601DateFormatter() + for fixture in fixtures { + checkTitles( + for: isoFormatter.date(from: fixture.timestamp)!, + calendar: fixture.calendar, + timeZone: fixture.timeZone, + expected: fixture.expected, + label: fixture.label + ) + } + + let englishMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + let englishWeekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + let koreanWeekdays = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"] + let japaneseWeekdays = ["日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"] + let today = Date() + let yesterday = calendar.date(byAdding: .day, value: -1, to: today)! + for (label, date) in [("today without relative wording", today), ("yesterday without relative wording", yesterday)] { + let components = calendar.dateComponents([.year, .month, .day, .weekday], from: date) + let year = components.year! + let month = components.month! + let day = components.day! + let weekday = components.weekday! - 1 + checkTitles( + for: date, + calendar: calendar, + timeZone: utc, + expected: [ + "\(year)년 \(month)월 \(day)일 \(koreanWeekdays[weekday])", + "\(englishWeekdays[weekday]), \(englishMonths[month - 1]) \(day), \(year)", + "\(year)年\(month)月\(day)日 \(japaneseWeekdays[weekday])" + ], + label: label + ) + } + print("PASS: \(assertions) assertions") + } +} diff --git a/work/tests/check-user-creator-chat-date-presentation.sh b/work/tests/check-user-creator-chat-date-presentation.sh new file mode 100644 index 00000000..46152871 --- /dev/null +++ b/work/tests/check-user-creator-chat-date-presentation.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +export TMPDIR="$tmp" +export CLANG_MODULE_CACHE_PATH="$tmp/clang-cache" +export SWIFT_MODULECACHE_PATH="$tmp/swift-cache" +export XDG_CACHE_HOME="$tmp/cache" + +swiftc -swift-version 5 -module-cache-path "$SWIFT_MODULECACHE_PATH" \ + "$root/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatModels.swift" \ + "$root/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDisplayMessage.swift" \ + "$root/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDatePresentation.swift" \ + "$root/work/tests/UserCreatorChatDatePresentationCheck.swift" \ + -o "$tmp/user-creator-chat-date-presentation-check" + +"$tmp/user-creator-chat-date-presentation-check"