feat(chat): 대화 날짜 구분선을 연결한다

This commit is contained in:
Yu Sung
2026-10-19 10:12:57 +09:00
parent e765c3c75c
commit 7a6d12e871
5 changed files with 185 additions and 8 deletions
@@ -21,6 +21,111 @@ enum UserCreatorChatDatePresentationCheck {
}
}
static func timestamp(_ value: String) -> Int64 {
Int64(ISO8601DateFormatter().date(from: value)!.timeIntervalSince1970 * 1000)
}
static func serverMessage(_ id: Int, at value: String, type: String = "TEXT") -> UserCreatorChatDisplayMessage {
UserCreatorChatDisplayMessage(message: UserCreatorChatMessageItem(
messageId: id,
messageType: type,
mine: true,
createdAt: timestamp(value),
textMessage: type == "TEXT" ? "message \(id)" : nil,
voiceMessageUrl: nil,
senderId: 1,
senderNickname: "sender",
senderProfileImageUrl: nil
))
}
static func checkRows(
_ messages: [UserCreatorChatDisplayMessage],
calendar: Calendar,
timeZone: TimeZone,
expectedMessages: [UserCreatorChatDisplayMessage],
separatorIndices: [Int],
label: String
) {
let rows = UserCreatorChatDatePresentation.rows(from: messages, calendar: calendar, timeZone: timeZone)
let expectedDates: [Date?] = expectedMessages.enumerated().map { index, message in
separatorIndices.contains(index)
? Date(timeIntervalSince1970: TimeInterval(message.createdAt) / 1000)
: nil
}
check(rows.map(\.id) == expectedMessages.map(\.id), "\(label): preserve message IDs and order")
check(rows.map(\.message) == expectedMessages, "\(label): preserve complete messages and status")
check(rows.map(\.separatorDate) == expectedDates, "\(label): exact separator positions and timestamps")
}
static func checkDatedRows(calendar: Calendar, utc: TimeZone, seoul: TimeZone, losAngeles: TimeZone) {
let first = serverMessage(1, at: "2026-10-16T10:00:00Z")
let second = serverMessage(2, at: "2026-10-16T11:00:00Z")
let third = serverMessage(3, at: "2026-10-16T12:00:00Z")
let previous = serverMessage(4, at: "2026-10-15T23:00:00Z")
let next = serverMessage(5, at: "2026-10-17T01:00:00Z")
checkRows([], calendar: calendar, timeZone: utc, expectedMessages: [], separatorIndices: [], label: "empty")
let singleRows = UserCreatorChatDatePresentation.rows(from: [first], calendar: calendar, timeZone: utc)
check(
singleRows.first?.separatorDate == Date(timeIntervalSince1970: TimeInterval(first.createdAt) / 1000),
"single TEXT message must have its first separatorDate"
)
checkRows([first], calendar: calendar, timeZone: utc, expectedMessages: [first], separatorIndices: [0], label: "single")
checkRows([second, third], calendar: calendar, timeZone: utc, expectedMessages: [second, third], separatorIndices: [0], label: "before prepend")
checkRows([first, second, third], calendar: calendar, timeZone: utc, expectedMessages: [first, second, third], separatorIndices: [0], label: "same day prepend moves first separator")
checkRows([previous, first, second, third], calendar: calendar, timeZone: utc, expectedMessages: [previous, first, second, third], separatorIndices: [0, 1], label: "different day prepend")
checkRows([previous, first, next], calendar: calendar, timeZone: utc, expectedMessages: [previous, first, next], separatorIndices: [0, 1, 2], label: "consecutive different days")
let utcBoundary = [serverMessage(6, at: "2026-10-15T23:30:00Z"), serverMessage(7, at: "2026-10-16T00:30:00Z")]
checkRows(utcBoundary, calendar: calendar, timeZone: seoul, expectedMessages: utcBoundary, separatorIndices: [0], label: "UTC midnight is same Seoul day with UTC calendar")
checkRows(utcBoundary, calendar: calendar, timeZone: utc, expectedMessages: utcBoundary, separatorIndices: [0, 1], label: "same timestamps cross UTC day")
let seoulBoundary = [serverMessage(8, at: "2026-10-16T14:30:00Z"), serverMessage(9, at: "2026-10-16T15:30:00Z")]
checkRows(seoulBoundary, calendar: calendar, timeZone: seoul, expectedMessages: seoulBoundary, separatorIndices: [0, 1], label: "Seoul midnight within same UTC day")
checkRows(seoulBoundary, calendar: calendar, timeZone: utc, expectedMessages: seoulBoundary, separatorIndices: [0], label: "same timestamps remain same UTC day")
var mismatchedCalendar = calendar
mismatchedCalendar.timeZone = seoul
checkRows(seoulBoundary, calendar: mismatchedCalendar, timeZone: utc, expectedMessages: seoulBoundary, separatorIndices: [0], label: "explicit UTC overrides Seoul calendar")
let repeatedHour = [serverMessage(10, at: "2026-11-01T08:30:00Z"), serverMessage(11, at: "2026-11-01T09:30:00Z")]
let beforeMidnight = serverMessage(12, at: "2026-11-02T07:30:00Z")
let midnight = serverMessage(13, at: "2026-11-02T08:00:00Z")
checkRows(repeatedHour, calendar: calendar, timeZone: losAngeles, expectedMessages: repeatedHour, separatorIndices: [0], label: "LA DST repeated 01:30")
let dstDay = repeatedHour + [beforeMidnight, midnight]
checkRows(dstDay, calendar: calendar, timeZone: losAngeles, expectedMessages: dstDay, separatorIndices: [0, 3], label: "LA 25-hour day ends at local midnight")
let earlierVoice = serverMessage(14, at: "2026-10-14T10:00:00Z", type: "VOICE")
let betweenVoice = serverMessage(15, at: "2026-10-15T10:00:00Z", type: "VOICE")
let laterVoice = serverMessage(16, at: "2026-10-18T10:00:00Z", type: "VOICE")
checkRows([earlierVoice, betweenVoice, laterVoice], calendar: calendar, timeZone: utc, expectedMessages: [], separatorIndices: [], label: "VOICE-only has no orphan dates")
checkRows([earlierVoice, first, betweenVoice, second, laterVoice], calendar: calendar, timeZone: utc, expectedMessages: [first, second], separatorIndices: [0], label: "mixed VOICE cannot split same TEXT day")
checkRows([earlierVoice, first, betweenVoice, next, laterVoice], calendar: calendar, timeZone: utc, expectedMessages: [first, next], separatorIndices: [0, 1], label: "mixed VOICE preserves only displayed day boundaries")
let pending = UserCreatorChatDisplayMessage(pendingText: "pending", requestId: "snapshot", createdAt: timestamp("2026-10-17T00:01:00Z"))
var failed = pending
failed.status = .failed
checkRows([first, pending], calendar: calendar, timeZone: utc, expectedMessages: [first, pending], separatorIndices: [0, 1], label: "pending local date snapshot")
checkRows([first, failed], calendar: calendar, timeZone: utc, expectedMessages: [first, failed], separatorIndices: [0, 1], label: "failed keeps pending date and ID")
let pendingDates = UserCreatorChatDatePresentation.rows(from: [first, pending], calendar: calendar, timeZone: utc).map(\.separatorDate)
let failedDates = UserCreatorChatDatePresentation.rows(from: [first, failed], calendar: calendar, timeZone: utc).map(\.separatorDate)
check(pendingDates == failedDates, "pending-to-failed changes no separator timestamps")
let serverSameDay = serverMessage(17, at: "2026-10-17T00:02:00Z")
let serverChangedDay = serverMessage(17, at: "2026-10-16T23:59:00Z")
checkRows([first, serverSameDay], calendar: calendar, timeZone: utc, expectedMessages: [first, serverSameDay], separatorIndices: [0, 1], label: "same day ACK replacement uses server timestamp and ID")
checkRows([first, serverChangedDay], calendar: calendar, timeZone: utc, expectedMessages: [first, serverChangedDay], separatorIndices: [0], label: "changed day ACK removes empty pending date and duplicate destination date")
checkRows([pending], calendar: calendar, timeZone: utc, expectedMessages: [pending], separatorIndices: [0], label: "only pending snapshot")
checkRows([serverChangedDay], calendar: calendar, timeZone: utc, expectedMessages: [serverChangedDay], separatorIndices: [0], label: "only pending replaced leaves only server date")
checkRows([first], calendar: calendar, timeZone: utc, expectedMessages: [first], separatorIndices: [0], label: "removed last day leaves no orphan")
checkRows([], calendar: calendar, timeZone: utc, expectedMessages: [], separatorIndices: [], label: "removed all messages leaves no orphan")
let equalTimestamp = serverMessage(18, at: "2026-10-16T10:00:00Z")
checkRows([equalTimestamp, first, second], calendar: calendar, timeZone: utc, expectedMessages: [equalTimestamp, first, second], separatorIndices: [0], label: "equal timestamps keep supplied order and IDs")
checkRows([next, first, second, previous, third], calendar: calendar, timeZone: utc, expectedMessages: [next, first, second, previous, third], separatorIndices: [0, 1, 3, 4], label: "only adjacent days compared without sorting")
checkRows([first, first], calendar: calendar, timeZone: utc, expectedMessages: [first, first], separatorIndices: [0], label: "input is not deduplicated")
let milliseconds = UserCreatorChatDisplayMessage(pendingText: "fraction", requestId: "milliseconds", createdAt: first.createdAt + 123)
checkRows([milliseconds], calendar: calendar, timeZone: utc, expectedMessages: [milliseconds], separatorIndices: [0], label: "millisecond timestamp precision")
}
static func main() {
let utc = TimeZone(secondsFromGMT: 0)!
let seoul = TimeZone(identifier: "Asia/Seoul")!
@@ -83,6 +188,7 @@ enum UserCreatorChatDatePresentationCheck {
label: label
)
}
checkDatedRows(calendar: calendar, utc: utc, seoul: seoul, losAngeles: losAngeles)
print("PASS: \(assertions) assertions")
}
}