Skip to content

[FEAT] 채팅 기능 추가 - #15

Merged
cchanmi merged 18 commits into
developfrom
feature/chat-messaging
Aug 10, 2026
Merged

[FEAT] 채팅 기능 추가#15
cchanmi merged 18 commits into
developfrom
feature/chat-messaging

Conversation

@cchanmi

@cchanmi cchanmi commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Describe

캐릭터와 대화하는 채팅 기능을 처음부터 끝까지 구현합니다. 대화 도메인 모델/네트워크 연동부터 온디바이스 대화 요약, 홈·대화 목록·채팅 화면, 인용 답장(대댓글) UI까지 포함합니다.

Works made

  • Domain: Message, EmotionCharacter, CommentRevealPolicy, ConversationSummaryPolicy, ConversationSummary, ConversationRepository/ConversationSummaryStore 프로토콜 추가
  • Data: ConversationEndpoint, ConversationMessageDTO/SaveMessageRequestDTO/SaveMessageResponseDTO/ConversationSummaryDTO, DefaultConversationRepository, LazyConversationSummaryStore(온디바이스 요약기 지연 초기화) 추가
  • UseCase: SendMessageUseCase, GetMessagesUseCase, GetConversationsUseCase 추가
  • Presentation
    • ChatView/ChatViewModel: 메시지 전송, 캐릭터 댓글 순차 노출, 답장 인용 미리보기 UI
    • HomeView/HomeViewModel: 홈에서 쪽지 전송 시 채팅방으로 바로 전달
    • ConversationListView/ConversationListViewModel: 대화 목록 조회
    • MainTabView: 탭 구조 추가
  • 버그 픽스
    • 댓글 순차 노출 중 취소 시 마지막 댓글이 유실되던 레이스 컨디션
    • restore() 시 짧은 대화 복원 후 첫 발화가 청크에 중복 편입되던 버그
    • 슬픔이 페르소나 emotionType 매핑을 서버 값(WARM)에 맞게 수정

Changes Made

As-Is

기존 로직
채팅/대화 관련 화면과 네트워크 연동이 없었고, 홈에서 쪽지를 보내도 대화가 이어지지 않았습니다.

To-BE

변경 로직
홈에서 쪽지를 보내면 채팅방으로 진입해 캐릭터들의 댓글이 순차적으로 표시되고, 답장 메시지는 원본 메시지를 인용해 보여줍니다. 대화 목록 탭에서 이전 대화도 조회할 수 있습니다.

How to Test

  1. 로그인 후 홈에서 쪽지를 보내면 채팅방으로 이동하며 캐릭터 댓글이 순차적으로 표시되는지 확인
  2. 채팅방에서 메시지를 여러 번 보내 답장 말풍선에 원본 메시지 인용문이 표시되는지 확인
  3. 대화 목록 탭에서 이전에 나눈 대화들이 조회되는지 확인
  4. 댓글이 순차 노출되는 중 화면을 벗어났다가 돌아와도 메시지가 유실되지 않는지 확인

Issues Resolved

Additional context

  • WARM/SAD emotionType 매핑 관련: 이전 커밋에서 서버 키를 SAD로 착각해 변경했다가, 서버가 실제로는 WARM을 계속 내려주는 것으로 확인되어 이번 PR에서 다시 WARM으로 수정했습니다.
  • 아직까지 키보드 활성화에 따른 레이아웃이 완전하지 않습니다. PR 규모가 커지는 것 같아 머지 후 디테일한 부분을 수정하려고 합니다.
  • 현재 브랜치 버전으로 테플 버전을 생성하여 팀원들과 프롬프트를 확인해 볼 예정입니다.

References

cchanmi added 16 commits August 7, 2026 15:30
- 최근창(3개) 이하로 대화를 복원하면 nextChunkCandidate가 0으로 설정되어
  이후 발화 추가 시 첫 발화가 청크 후보로 재편입되던 문제를 최소값 1로 고정
- 회귀 테스트 추가
- 전송 버튼이 전송 중(isSending)에도 비활성화되도록 수정
서버 emotionType 매핑을 WARM→SAD로 변경하고, 채팅 답장 버블에 아바타와
페르소나 이름 라벨을 붙여 어떤 감정이 답장했는지 구분되도록 함.
@cchanmi cchanmi changed the title [FEAT] 채팅(대화) 기능 추가 [FEAT] 채팅 기능 추가 Aug 9, 2026
- develop의 Endpoint 구조 리팩토링(Foundation 폴더링, RequestParameter/HTTPHeader 분리) 반영
- ConversationEndpoint를 새 Endpoint 프로토콜(parameters, header)에 맞게 수정
@cchanmi cchanmi self-assigned this Aug 9, 2026

@dlrjswns dlrjswns left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cchanmi
수고많으셨습니다 !! 일부 로직 부분만 수정해주시고 현재 제가 만든 ChatEndpoint와 찬미님이 만드신 ConversationEndpoint가 겹치는데 이 부분 사용안하신다면 삭제하셔도 좋을꺼같아요 !!

Comment on lines +14 to +15
private let conversationId: Int?
private let initialSentMessage: SentMessage?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1
저희 구조가 UseCase와 ViewModel이 있어서 해당 값은 View에서 가지고있기보다 다른 영역에서 다루는게 좋을꺼같아요 !!

Comment on lines +30 to +39
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())
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1
기존 작성된 DateFormatterFactory를 사용해주세요 !!

Comment on lines +58 to +69
.alert(
viewModel.toastMessage ?? "",
isPresented: Binding(
get: {
viewModel.toastMessage != nil
},
set: {
if !$0 {
viewModel.toastMessage = nil
}
}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적으로 alert를 띄우는데 toastMessage라는게 조금 상이한 의미같아서 다른 네이밍을 이용해보는건 어떨까요 ?

Comment on lines +106 to +122
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))
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1
화면에서 사용자 입력을 관리하는 함수같은데 특정 입력을 잘라주는 로직은 ViewModel에 두는게 맞는거같아요 !!

Comment on lines +94 to +100
.disabled(
viewModel.input
.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty
|| viewModel.isSending
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1
이 부분도 UI동작에 관련된 상태값이라 ViewModel에서 처리하는게 맞는거같아요 !!

return
}

let maxLength = ConversationSummaryPolicy.maxMessageLength

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task.sleep을 작성한 이유가 궁금해요 !!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

순차적으로 댓글이 보여져야 해서 사용을 했는데 채팅 부분은 아직 부자연스러운 부분이 많아서 추가 수정이 필요합니다

Comment on lines +106 to +118
.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))
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 위와 동일합니다 !!

Comment on lines +139 to +140
.disabled(viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSending)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분또한 위와 동일합니다

Comment on lines +8 to +21
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: "엉뚱이"
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cchanmi
EmotionCharacter가 Domain Entity로 만드신 Emotion이랑 어떤점이 다른건가요 ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emotion은 온디바이스 감정분석 모델이 사용자 발화를 분석해서 내놓는 6가지 감정 분류값이고, EmotionCharacter는 서버가 대화 메시지의 발신자로 내려주는 페르소나 캐릭터 6종이에요

둘 다 case가 6개라 같은 걸로 보일 수 있는데 실제로 감정 종류가 다릅니당

@cchanmi

cchanmi commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

p1은 모두 반영하였습니다

@cchanmi
cchanmi merged commit 8bf3b17 into develop Aug 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants