feat(chat): 유저 크리에이터 DM을 추가한다

This commit is contained in:
Yu Sung
2026-07-12 04:48:17 +09:00
parent ce191539f2
commit 2dfff32ac1
23 changed files with 2410 additions and 6 deletions

View File

@@ -0,0 +1,202 @@
import SwiftUI
import UIKit
struct UserCreatorChatRoomView: View {
@StateObject private var viewModel = UserCreatorChatRoomViewModel()
@State private var messageText = ""
private let roomId: Int?
private let creatorId: Int?
init(roomId: Int) {
self.roomId = roomId
self.creatorId = nil
}
init(creatorId: Int) {
self.roomId = nil
self.creatorId = creatorId
}
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
headerView
messageListView
.frame(maxWidth: .infinity, maxHeight: .infinity)
inputBarView
}
.background(Color.black.ignoresSafeArea())
}
.onAppear(perform: enterRoom)
.onDisappear { viewModel.leaveAndClose() }
.sodaToast(
isPresented: $viewModel.isShowPopup,
message: viewModel.errorMessage,
autohideIn: 2
)
}
private var headerView: some View {
HStack(spacing: SodaSpacing.s12) {
Image("ic_back")
.resizable()
.frame(width: 24, height: 24)
.onTapGesture { AppState.shared.back() }
DownsampledKFImage(
url: URL(string: viewModel.opponentProfileImageUrl ?? ""),
size: CGSize(width: 36, height: 36)
)
.clipShape(Circle())
Text(viewModel.opponentNickname)
.appFont(size: 14, weight: .bold)
.foregroundColor(.white)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 0)
}
.padding(.horizontal, SodaSpacing.s16)
.padding(.vertical, SodaSpacing.s8)
.frame(width: screenSize().width, height: 60)
.background(Color.black)
}
private var messageListView: some View {
GeometryReader { geometry in
ScrollViewReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: SodaSpacing.s16) {
if viewModel.isLoadingNextPage {
ProgressView()
.progressViewStyle(.circular)
.tint(Color.soda400)
.padding(.vertical, SodaSpacing.s12)
}
ForEach(textMessages) { message in
UserCreatorChatTextMessageItemView(message: message)
.id(message.id)
.onAppear {
if message.id == textMessages.first?.id {
viewModel.loadMore()
}
}
}
}
.padding(.horizontal, SodaSpacing.s24)
.padding(.vertical, SodaSpacing.s12)
.background(
GeometryReader { contentGeometry in
Color.clear.preference(
key: UserCreatorChatContentHeightPreferenceKey.self,
value: contentGeometry.size.height
)
}
)
.frame(minHeight: geometry.size.height, alignment: .bottom)
}
.onPreferenceChange(UserCreatorChatContentHeightPreferenceKey.self) { contentHeight in
guard contentHeight <= geometry.size.height else { return }
viewModel.loadMore()
}
.onChange(of: textMessages.last?.id) { _ in
scrollToBottom(proxy)
}
.onChange(of: viewModel.socketState) { state in
guard state == .joined else { return }
scrollToBottom(proxy)
}
}
}
}
private var inputBarView: some View {
HStack(spacing: SodaSpacing.s8) {
ZStack(alignment: .leading) {
if messageText.isEmpty {
Text(I18n.UserCreatorChat.messagePlaceholder)
.appFont(size: 14, weight: .regular)
.foregroundColor(Color.gray500)
}
TextField("", text: $messageText)
.appFont(size: 14, weight: .regular)
.foregroundColor(.white)
.accentColor(Color.soda400)
.disabled(isInputEnabled == false)
.onSubmit { sendText() }
}
.padding(.horizontal, SodaSpacing.s16)
.padding(.vertical, SodaSpacing.s12)
.background(Color.gray900)
.cornerRadius(999)
Button(action: sendText) {
Image("ic_message_send")
.resizable()
.frame(width: 24, height: 24)
.opacity(isSendEnabled ? 1 : 0.4)
}
.buttonStyle(.plain)
.disabled(isSendEnabled == false)
}
.padding(.horizontal, SodaSpacing.s12)
.padding(.vertical, SodaSpacing.s12)
.frame(width: screenSize().width)
.background(Color.black)
}
private var textMessages: [UserCreatorChatDisplayMessage] {
viewModel.messages.filter { $0.messageType == "TEXT" }
}
private var isInputEnabled: Bool {
viewModel.socketState == .joined
}
private var isSendEnabled: Bool {
isInputEnabled && messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
}
private func enterRoom() {
if let roomId {
viewModel.enter(roomId: roomId)
} else if let creatorId {
viewModel.enter(creatorId: creatorId)
}
}
private func sendText() {
let trimmedText = messageText.trimmingCharacters(in: .whitespacesAndNewlines)
guard isInputEnabled, trimmedText.isEmpty == false else { return }
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
viewModel.sendText(trimmedText)
messageText = ""
}
private func scrollToBottom(_ proxy: ScrollViewProxy) {
guard let lastMessage = textMessages.last else { return }
withAnimation(.easeOut(duration: 0.3)) {
proxy.scrollTo(lastMessage.id, anchor: .bottom)
}
}
}
private struct UserCreatorChatContentHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct UserCreatorChatRoomView_Previews: PreviewProvider {
static var previews: some View {
UserCreatorChatRoomView(roomId: 1)
}
}