Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions GAMSS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@
3D1B542CB60C3958D3F16702 /* EmotionCharacterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB1A4112363D93ED3AD1598 /* EmotionCharacterTests.swift */; };
3EF2F6DB34A2276C2C9FD746 /* diary_samples.json in Resources */ = {isa = PBXBuildFile; fileRef = ED231F0A80BD3A4AF9E50BBC /* diary_samples.json */; };
573CFD0A1638F27B889EF865 /* SendMessageUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDF78D0118869F3D9E1E269 /* SendMessageUseCaseTests.swift */; };
58652E08F4413DEA2016C082 /* MessageBubbleLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF457567BE11A68704DC4FE /* MessageBubbleLayoutTests.swift */; };
5E6EF3C5D7CE4FF3EE4392EC /* ConversationMessageDTOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35175B780592B411BDD6B82 /* ConversationMessageDTOTests.swift */; };
5F8A6F3F851E20BCFE3F805D /* CommentRevealPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86563A77A8A674E7F050119E /* CommentRevealPolicyTests.swift */; };
66BC114B170E43E9C900F517 /* QuotedReplyHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B68677BF01B5A8971B85AC4 /* QuotedReplyHeaderTests.swift */; };
70248D85300794C800857369 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 70248D84300794C800857369 /* FirebaseCrashlytics */; };
70248DCD300D429100857369 /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 70248DCC300D429100857369 /* FirebaseAuth */; };
8F4E518B79C7B76223B098BA /* Tokenizers in Frameworks */ = {isa = PBXBuildFile; productRef = 8E8487F478AF703D394C5EB6 /* Tokenizers */; };
94A526BCCCD9DF730A39BF97 /* sample_sentences.json in Resources */ = {isa = PBXBuildFile; fileRef = D4BE9D664889F13369325DC9 /* sample_sentences.json */; };
9EA6F6B8DF51021E18342083 /* HomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EDC07CB1FA5BA970388DB6A /* HomeViewModelTests.swift */; };
FA90ADA39E7A145713881061 /* MainTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C15D652D0472A1EBD024674 /* MainTabTests.swift */; };
A8948FBCA4DF4E43F2C582EA /* Pods_GAMSS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB14F9D00BB8B80EFE9A40A4 /* Pods_GAMSS.framework */; };
A8FEE0AD9A4A60908DC5F5F1 /* CreateMessageRequestDTOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DAA522A6DCECC0B3666523C /* CreateMessageRequestDTOTests.swift */; };
A9E97BA8DEC5EE7635B77D1F /* ChatComposerViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5DF3BD54F67AE200949A44 /* ChatComposerViewTests.swift */; };
ABF5C91182F3B409C1AA9E8E /* GetMessagesUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A67D7BECCB85A88C969AD7 /* GetMessagesUseCaseTests.swift */; };
B32ACCF04698A89364EDDECE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 316EA002FAC5D3328796EF90 /* Foundation.framework */; };
BA7748118B0D2759E0F6A207 /* Pods_GAMSSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F1CC8FA289575E4B98C41539 /* Pods_GAMSSTests.framework */; };
Expand All @@ -36,10 +38,12 @@
CC604C7607382102024E0FA1 /* ConversationSummaryPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CCBD8A019CAA37319E7984 /* ConversationSummaryPolicyTests.swift */; };
DC1369FEAE93D794107D5373 /* Color_GAMSSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596EE0F239D37460A7BA389D /* Color_GAMSSTests.swift */; };
E3A3BE7330A1CCAB9E144B41 /* DefaultEmotionAnalysisRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B816EE4A02D0B23C50F35FF9 /* DefaultEmotionAnalysisRepositoryTests.swift */; };
EA9C48AD6033E92B4734D506 /* MessageTimestampFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5370238416571DC93C29121 /* MessageTimestampFormatterTests.swift */; };
EE5555555555555555555555 /* TypographyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4444444444444444444444 /* TypographyTests.swift */; };
F1D4E5A20F2B3C4D5E6F7A8B /* SpacingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D4E5A30F2B3C4D5E6F7A8C /* SpacingTests.swift */; };
F29C98F332A3EC127B809DD3 /* ChatViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE5C85E76690E41AE394895D /* ChatViewModelTests.swift */; };
F2D4E5A40F2B3C4D5E6F7A9D /* RadiusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D4E5A50F2B3C4D5E6F7A9E /* RadiusTests.swift */; };
FA90ADA39E7A145713881061 /* MainTabTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C15D652D0472A1EBD024674 /* MainTabTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand All @@ -56,13 +60,16 @@
032B0D990023EAA1E72E1E82 /* DefaultConversationSummaryStoreTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DefaultConversationSummaryStoreTests.swift; sourceTree = "<group>"; };
162D28B313B7D3B6D79FE039 /* SummaryErrorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SummaryErrorTests.swift; sourceTree = "<group>"; };
1A8F630A15A80F5FAB52F2C1 /* Pods-GAMSSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GAMSSTests.debug.xcconfig"; path = "Target Support Files/Pods-GAMSSTests/Pods-GAMSSTests.debug.xcconfig"; sourceTree = "<group>"; };
1B68677BF01B5A8971B85AC4 /* QuotedReplyHeaderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QuotedReplyHeaderTests.swift; sourceTree = "<group>"; };
316EA002FAC5D3328796EF90 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
3EDC07CB1FA5BA970388DB6A /* HomeViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeViewModelTests.swift; sourceTree = "<group>"; };
3C15D652D0472A1EBD024674 /* MainTabTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabTests.swift; sourceTree = "<group>"; };
3C5DF3BD54F67AE200949A44 /* ChatComposerViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChatComposerViewTests.swift; sourceTree = "<group>"; };
3EDC07CB1FA5BA970388DB6A /* HomeViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeViewModelTests.swift; sourceTree = "<group>"; };
41CCBD8A019CAA37319E7984 /* ConversationSummaryPolicyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConversationSummaryPolicyTests.swift; sourceTree = "<group>"; };
4DDF78D0118869F3D9E1E269 /* SendMessageUseCaseTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SendMessageUseCaseTests.swift; sourceTree = "<group>"; };
596EE0F239D37460A7BA389D /* Color_GAMSSTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Color_GAMSSTests.swift; sourceTree = "<group>"; };
5B4F594F8D1059D3DF30DE92 /* GAMSSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GAMSSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6EF457567BE11A68704DC4FE /* MessageBubbleLayoutTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessageBubbleLayoutTests.swift; sourceTree = "<group>"; };
708F57F33003C651008DF433 /* GAMSS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GAMSS.app; sourceTree = BUILT_PRODUCTS_DIR; };
7674D757C326ABEDB6302AA6 /* GAMSSTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GAMSSTests.swift; sourceTree = "<group>"; };
79F056320618BF2744EAC909 /* EmotionTestViewModelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EmotionTestViewModelTests.swift; sourceTree = "<group>"; };
Expand All @@ -83,6 +90,7 @@
CEB4219C48B0B2A51FA9457C /* EmotionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EmotionTests.swift; sourceTree = "<group>"; };
D4BE9D664889F13369325DC9 /* sample_sentences.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; path = sample_sentences.json; sourceTree = "<group>"; };
DD4444444444444444444444 /* TypographyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TypographyTests.swift; sourceTree = "<group>"; };
E5370238416571DC93C29121 /* MessageTimestampFormatterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessageTimestampFormatterTests.swift; sourceTree = "<group>"; };
E7BE3E28AC7871B572DD4B7E /* Pods-GAMSSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GAMSSTests.release.xcconfig"; path = "Target Support Files/Pods-GAMSSTests/Pods-GAMSSTests.release.xcconfig"; sourceTree = "<group>"; };
EAD22D6FD3BB2E8E689A52BE /* Pods-GAMSS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GAMSS.debug.xcconfig"; path = "Target Support Files/Pods-GAMSS/Pods-GAMSS.debug.xcconfig"; sourceTree = "<group>"; };
EB14F9D00BB8B80EFE9A40A4 /* Pods_GAMSS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GAMSS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -280,6 +288,10 @@
BE5C85E76690E41AE394895D /* ChatViewModelTests.swift */,
3EDC07CB1FA5BA970388DB6A /* HomeViewModelTests.swift */,
3C15D652D0472A1EBD024674 /* MainTabTests.swift */,
E5370238416571DC93C29121 /* MessageTimestampFormatterTests.swift */,
1B68677BF01B5A8971B85AC4 /* QuotedReplyHeaderTests.swift */,
6EF457567BE11A68704DC4FE /* MessageBubbleLayoutTests.swift */,
3C5DF3BD54F67AE200949A44 /* ChatComposerViewTests.swift */,
);
path = Presentation;
sourceTree = "<group>";
Expand Down Expand Up @@ -588,6 +600,10 @@
A8FEE0AD9A4A60908DC5F5F1 /* CreateMessageRequestDTOTests.swift in Sources */,
5E6EF3C5D7CE4FF3EE4392EC /* ConversationMessageDTOTests.swift in Sources */,
C8DE812C64C04B4EE47E3636 /* DefaultConversationRepositoryTests.swift in Sources */,
EA9C48AD6033E92B4734D506 /* MessageTimestampFormatterTests.swift in Sources */,
66BC114B170E43E9C900F517 /* QuotedReplyHeaderTests.swift in Sources */,
58652E08F4413DEA2016C082 /* MessageBubbleLayoutTests.swift in Sources */,
A9E97BA8DEC5EE7635B77D1F /* ChatComposerViewTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
4 changes: 4 additions & 0 deletions GAMSS/Sources/App/GAMSSApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ struct GAMSSApp: App {
var body: some Scene {
WindowGroup {
MainTabView()
.preferredColorScheme(.light)
}
// 디자인 토큰이 다크모드 값을 갖고 있지만 디자이너가 아직 다크모드 화면을 설계하지
// 않았다 — 시스템 다크모드를 그대로 따라가면 색상이 라이트/다크 목적과 반대로
// 뒤집혀 보이므로 앱 전체를 라이트 모드로 고정한다.
}
}
38 changes: 34 additions & 4 deletions GAMSS/Sources/Data/DTO/ConversationMessageDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import Foundation

/// 서버 필드명과 1:1 (Android ConversationMessage.kt 기준). senderType이 "CHARACTER"일 때만
/// emotionType이 채워진다. senderType이 알 수 없거나 emotionType을 못 알아보면 toDomain()이
/// nil을 반환 — 잘못된 주체로 그리는 것보다 목록에서 제외하는 쪽을 택한다.
/// nil을 반환 — 잘못된 주체로 그리는 것보다 목록에서 제외하는 쪽을 택한다. createdAt 파싱
/// 실패도 같은 이유로 nil을 반환한다.
struct ConversationMessageDTO: Decodable {
let id: Int
let conversationId: Int
Expand All @@ -25,6 +26,25 @@ struct ConversationMessageDTO: Decodable {
"GRUMPY": .prickly, "WARM": .sadness, "QUIRKY": .quirky,
]

/// 화면에는 어차피 분 단위까지만 표시하므로(MessageTimestampFormatter), 소수초 정밀도는
/// 필요 없다. 서버가 소수초 포함 타임스탬프(예: "2026-08-07T00:00:00.123Z")를 보내더라도
/// 포매터를 두 개 두고 두 번 시도하는 대신, 소수초 부분만 잘라내고 포매터 하나로 파싱한다.
/// ISO8601DateFormatter는 동시 접근 안전성이 문서화되어 있지 않아 인스턴스를 static으로
/// 공유하지 않고 호출마다 새로 만든다 — 히스토리 로드와 메시지 전송 응답이 동시에 디코딩될
/// 수 있기 때문.
private static func parseCreatedAt(_ value: String) -> Date? {
ISO8601DateFormatter().date(from: stripFractionalSeconds(value))
}

/// 소수초 부분(".123" 등)만 제거한다. 뒤에 타임존 지정자(Z/+/-)가 없어도 소수초 자체는
/// 잘라내야 하므로, 그 존재를 전제하지 않고 "."부터 이어지는 숫자만 건너뛴다.
private static func stripFractionalSeconds(_ value: String) -> String {
guard let dotIndex = value.firstIndex(of: ".") else { return value }
let afterDot = value[value.index(after: dotIndex)...]
let suffixStart = afterDot.firstIndex(where: { !$0.isNumber }) ?? afterDot.endIndex
return String(value[..<dotIndex]) + String(afterDot[suffixStart...])
}

func toDomain() -> Message? {
let sender: MessageSender
switch senderType {
Expand All @@ -36,11 +56,21 @@ struct ConversationMessageDTO: Decodable {
default:
return nil
}
return Message(id: id, conversationId: conversationId, sender: sender, content: content, repliesToMessageId: repliesToMessageId)
guard let createdAtDate = Self.parseCreatedAt(createdAt) else { return nil }
return Message(id: id, conversationId: conversationId, sender: sender, content: content, repliesToMessageId: repliesToMessageId, createdAt: createdAtDate)
}

/// 방금 보낸 사용자 메시지 응답 전용 — 항상 .user로 간주한다.
/// 방금 보낸 사용자 메시지 응답 전용 — 항상 .user로 간주한다. createdAt 파싱에 실패해도
/// (반환 타입이 옵셔널이 아니라) 크래시 없이 현재 시각으로 대체한다 — 서버가 방금 만든
/// 응답이라 실제로 실패할 일은 거의 없는 안전망일 뿐이다.
func toSentUserMessage() -> Message {
Message(id: id, conversationId: conversationId, sender: .user, content: content, repliesToMessageId: repliesToMessageId)
Message(
id: id,
conversationId: conversationId,
sender: .user,
content: content,
repliesToMessageId: repliesToMessageId,
createdAt: Self.parseCreatedAt(createdAt) ?? Date()
)
}
}
3 changes: 3 additions & 0 deletions GAMSS/Sources/Domain/Conversation/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// Created by cchanmi on 8/7/26.
//

import Foundation

enum MessageSender: Equatable {
case user
case character(EmotionCharacter)
Expand All @@ -16,6 +18,7 @@ struct Message: Identifiable, Equatable {
let sender: MessageSender
let content: String
let repliesToMessageId: Int?
let createdAt: Date
}

enum CommentGenerationStatus: Equatable {
Expand Down
Loading
Loading