[FEAT] 채팅 기능 추가 - #15
Conversation
- 최근창(3개) 이하로 대화를 복원하면 nextChunkCandidate가 0으로 설정되어 이후 발화 추가 시 첫 발화가 청크 후보로 재편입되던 문제를 최소값 1로 고정 - 회귀 테스트 추가 - 전송 버튼이 전송 중(isSending)에도 비활성화되도록 수정
서버 emotionType 매핑을 WARM→SAD로 변경하고, 채팅 답장 버블에 아바타와 페르소나 이름 라벨을 붙여 어떤 감정이 답장했는지 구분되도록 함.
- develop의 Endpoint 구조 리팩토링(Foundation 폴더링, RequestParameter/HTTPHeader 분리) 반영 - ConversationEndpoint를 새 Endpoint 프로토콜(parameters, header)에 맞게 수정
| private let conversationId: Int? | ||
| private let initialSentMessage: SentMessage? |
There was a problem hiding this comment.
p1
저희 구조가 UseCase와 ViewModel이 있어서 해당 값은 View에서 가지고있기보다 다른 영역에서 다루는게 좋을꺼같아요 !!
| private static func todayDateString() -> String { | ||
| var calendar = Calendar(identifier: .gregorian) | ||
| calendar.timeZone = TimeZone(identifier: "Asia/Seoul") ?? .current | ||
| let formatter = DateFormatter() | ||
| formatter.calendar = calendar | ||
| formatter.timeZone = calendar.timeZone | ||
| formatter.dateFormat = "yyyy-MM-dd" | ||
| return formatter.string(from: Date()) | ||
| } | ||
| } |
There was a problem hiding this comment.
p1
기존 작성된 DateFormatterFactory를 사용해주세요 !!
| .alert( | ||
| viewModel.toastMessage ?? "", | ||
| isPresented: Binding( | ||
| get: { | ||
| viewModel.toastMessage != nil | ||
| }, | ||
| set: { | ||
| if !$0 { | ||
| viewModel.toastMessage = nil | ||
| } | ||
| } | ||
| ) |
There was a problem hiding this comment.
개인적으로 alert를 띄우는데 toastMessage라는게 조금 상이한 의미같아서 다른 네이밍을 이용해보는건 어떨까요 ?
| private func handleInputChange(_ newValue: String) { | ||
| // return 입력 시 줄바꿈을 제거하고 키보드를 내린다. | ||
| if newValue.hasSuffix("\n") { | ||
| viewModel.input = String(newValue.dropLast()) | ||
|
|
||
| // 별도의 animation 없이 포커스만 해제한다. | ||
| // keyboard dismiss와 layout animation이 충돌하는 것을 방지한다. | ||
| isInputFocused = false | ||
| return | ||
| } | ||
|
|
||
| let maxLength = ConversationSummaryPolicy.maxMessageLength | ||
|
|
||
| if newValue.count > maxLength { | ||
| viewModel.input = String(newValue.prefix(maxLength)) | ||
| } | ||
| } |
There was a problem hiding this comment.
p1
화면에서 사용자 입력을 관리하는 함수같은데 특정 입력을 잘라주는 로직은 ViewModel에 두는게 맞는거같아요 !!
| .disabled( | ||
| viewModel.input | ||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||
| .isEmpty | ||
| || viewModel.isSending | ||
| ) | ||
| } |
There was a problem hiding this comment.
p1
이 부분도 UI동작에 관련된 상태값이라 ViewModel에서 처리하는게 맞는거같아요 !!
| return | ||
| } | ||
|
|
||
| let maxLength = ConversationSummaryPolicy.maxMessageLength |
There was a problem hiding this comment.
p1
이런 텍스트 한도부분이 ViewModel에서 UX용 입력 제한용으로 사용하고 UseCase에서 최종 검증을 하는 방향으로 구현하면 추후에 한도변경이 되더라도 손쉽게 변경할 수 있을꺼같아요 !!
| // pendingComments에서 빼지 않고 들여다보기만 한다(제거는 취소 검사 통과 후에만). | ||
| let hasNext: Bool = await MainActor.run { !self.pendingComments.isEmpty } | ||
| guard hasNext else { break } | ||
| try? await Task.sleep(nanoseconds: UInt64(CommentRevealPolicy.nextGapSeconds() * 1_000_000_000)) |
There was a problem hiding this comment.
Task.sleep을 작성한 이유가 궁금해요 !!
There was a problem hiding this comment.
순차적으로 댓글이 보여져야 해서 사용을 했는데 채팅 부분은 아직 부자연스러운 부분이 많아서 추가 수정이 필요합니다
| .onChange(of: viewModel.input) { _, newValue in | ||
| // iOS 키보드의 return 키는 TextEditor에서 줄바꿈으로 들어온다 — | ||
| // 그 줄바꿈을 감지해 지우고 대신 키보드를 내린다. | ||
| if newValue.hasSuffix("\n") { | ||
| viewModel.input = String(newValue.dropLast()) | ||
| isInputFocused = false | ||
| return | ||
| } | ||
| if newValue.count > ConversationSummaryPolicy.maxMessageLength { | ||
| viewModel.input = String(newValue.prefix(ConversationSummaryPolicy.maxMessageLength)) | ||
| } | ||
| } | ||
| } |
| .disabled(viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending) | ||
| } |
| enum EmotionCharacter: CaseIterable, Equatable { | ||
| case joy, sadness, anger, anxiety, prickly, quirky | ||
|
|
||
| var displayName: String { | ||
| switch self { | ||
| case .joy: "기쁨이" | ||
| case .sadness: "슬픔이" | ||
| case .anger: "분노" | ||
| case .anxiety: "불안" | ||
| case .prickly: "까칠이" | ||
| case .quirky: "엉뚱이" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
@cchanmi
EmotionCharacter가 Domain Entity로 만드신 Emotion이랑 어떤점이 다른건가요 ?
There was a problem hiding this comment.
Emotion은 온디바이스 감정분석 모델이 사용자 발화를 분석해서 내놓는 6가지 감정 분류값이고, EmotionCharacter는 서버가 대화 메시지의 발신자로 내려주는 페르소나 캐릭터 6종이에요
둘 다 case가 6개라 같은 걸로 보일 수 있는데 실제로 감정 종류가 다릅니당
|
p1은 모두 반영하였습니다 |
Describe
캐릭터와 대화하는 채팅 기능을 처음부터 끝까지 구현합니다. 대화 도메인 모델/네트워크 연동부터 온디바이스 대화 요약, 홈·대화 목록·채팅 화면, 인용 답장(대댓글) UI까지 포함합니다.
Works made
Message,EmotionCharacter,CommentRevealPolicy,ConversationSummaryPolicy,ConversationSummary,ConversationRepository/ConversationSummaryStore프로토콜 추가ConversationEndpoint,ConversationMessageDTO/SaveMessageRequestDTO/SaveMessageResponseDTO/ConversationSummaryDTO,DefaultConversationRepository,LazyConversationSummaryStore(온디바이스 요약기 지연 초기화) 추가SendMessageUseCase,GetMessagesUseCase,GetConversationsUseCase추가ChatView/ChatViewModel: 메시지 전송, 캐릭터 댓글 순차 노출, 답장 인용 미리보기 UIHomeView/HomeViewModel: 홈에서 쪽지 전송 시 채팅방으로 바로 전달ConversationListView/ConversationListViewModel: 대화 목록 조회MainTabView: 탭 구조 추가restore()시 짧은 대화 복원 후 첫 발화가 청크에 중복 편입되던 버그emotionType매핑을 서버 값(WARM)에 맞게 수정Changes Made
As-Is
기존 로직
채팅/대화 관련 화면과 네트워크 연동이 없었고, 홈에서 쪽지를 보내도 대화가 이어지지 않았습니다.
To-BE
변경 로직
홈에서 쪽지를 보내면 채팅방으로 진입해 캐릭터들의 댓글이 순차적으로 표시되고, 답장 메시지는 원본 메시지를 인용해 보여줍니다. 대화 목록 탭에서 이전 대화도 조회할 수 있습니다.
How to Test
Issues Resolved
Additional context
WARM/SADemotionType 매핑 관련: 이전 커밋에서 서버 키를SAD로 착각해 변경했다가, 서버가 실제로는WARM을 계속 내려주는 것으로 확인되어 이번 PR에서 다시WARM으로 수정했습니다.References