diff --git a/SodaLive/Sources/Chat/Talk/Room/ChatRoomRepository.swift b/SodaLive/Sources/Chat/Talk/Room/ChatRoomRepository.swift index 9c9f8b9..bb946ba 100644 --- a/SodaLive/Sources/Chat/Talk/Room/ChatRoomRepository.swift +++ b/SodaLive/Sources/Chat/Talk/Room/ChatRoomRepository.swift @@ -58,4 +58,8 @@ class ChatRoomRepository { func purchaseChatQuota() -> AnyPublisher { return talkApi.requestPublisher(.purchaseChatQuota(request: ChatQuotaPurchaseRequest())) } + + func resetChatRoom(roomId: Int) -> AnyPublisher { + return talkApi.requestPublisher(.resetChatRoom(roomId: roomId, request: ChatRoomResetRequest())) + } } diff --git a/SodaLive/Sources/Chat/Talk/Room/ChatRoomView.swift b/SodaLive/Sources/Chat/Talk/Room/ChatRoomView.swift index 3d57f34..8d3ee93 100644 --- a/SodaLive/Sources/Chat/Talk/Room/ChatRoomView.swift +++ b/SodaLive/Sources/Chat/Talk/Room/ChatRoomView.swift @@ -19,202 +19,208 @@ struct ChatRoomView: View { var body: some View { BaseView(isLoading: $viewModel.isLoading) { - ChatRoomBgView(url: viewModel.chatRoomBgImageUrl) + if !viewModel.isHideBg { + ChatRoomBgView(url: viewModel.chatRoomBgImageUrl) + } - VStack(spacing: 0) { - HStack(spacing: 12) { - Image("ic_back") - .resizable() - .frame(width: 24, height: 24) - .onTapGesture { - AppState.shared.back() - } - - KFImage(URL(string: viewModel.characterProfileUrl)) - .placeholder { - Image(systemName: "person.crop.circle") - .resizable() - .scaledToFit() - } - .resizable() - .frame(width: 36, height: 36) - .clipShape(Circle()) - - VStack(alignment: .leading, spacing: 4) { - Text(viewModel.characterName) - .font(.custom(Font.preBold.rawValue, size: 12)) - .foregroundColor(.white) - .lineLimit(1) - .truncationMode(.tail) + if !viewModel.isResetting { + VStack(spacing: 0) { + HStack(spacing: 12) { + Image("ic_back") + .resizable() + .frame(width: 24, height: 24) + .onTapGesture { + AppState.shared.back() + } - Text(viewModel.characterType.rawValue) - .font(.custom(Font.preBold.rawValue, size: 10)) - .foregroundColor(.white) - .padding(.horizontal, 4) - .padding(.vertical, 2) - .background( - Color(hex: - viewModel.characterType == .Clone ? - "0020C9" : "009D68" - ) - ) - .cornerRadius(6) + KFImage(URL(string: viewModel.characterProfileUrl)) + .placeholder { + Image(systemName: "person.crop.circle") + .resizable() + .scaledToFit() + } + .resizable() + .frame(width: 36, height: 36) + .clipShape(Circle()) + + VStack(alignment: .leading, spacing: 4) { + Text(viewModel.characterName) + .font(.custom(Font.preBold.rawValue, size: 12)) + .foregroundColor(.white) + .lineLimit(1) + .truncationMode(.tail) + + Text(viewModel.characterType.rawValue) + .font(.custom(Font.preBold.rawValue, size: 10)) + .foregroundColor(.white) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .background( + Color(hex: + viewModel.characterType == .Clone ? + "0020C9" : "009D68" + ) + ) + .cornerRadius(6) + } + + Spacer() + + HStack(spacing: 4) { + Image("ic_can") + .resizable() + .frame(width: 20, height: 20) + + Text("\(can)") + .font(.custom(Font.preRegular.rawValue, size: 16)) + .foregroundColor(.white) + } + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background(Color(hex: "263238")) + .cornerRadius(30) + + Image("ic_seemore_vertical_white") + .resizable() + .frame(width: 24, height: 24) + .onTapGesture { + viewModel.isShowingChatSettingsView = true + } } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .frame(width: screenSize().width, height: 60) - Spacer() - - HStack(spacing: 4) { - Image("ic_can") + HStack(spacing: 8) { + Image(systemName: "info.circle.fill") .resizable() .frame(width: 20, height: 20) - Text("\(can)") - .font(.custom(Font.preRegular.rawValue, size: 16)) - .foregroundColor(.white) + Text( + viewModel.characterType == .Character + ? "보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.\n※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다." + : "AI Clone은 크리에이터의 정보를 기반으로 대화하지만, 모든 정보를 완벽하게 반영하거나 실제 대화와 일치하지 않을 수 있습니다." + ) + .font(.custom(Font.preRegular.rawValue, size: 12)) + .foregroundColor(.white) + + Image(systemName: "chevron.up") + .resizable() + .scaledToFit() + .frame(width: 20) } - .padding(.horizontal, 10) - .padding(.vertical, 5) - .background(Color(hex: "263238")) - .cornerRadius(30) + .padding(12) + .background(Color(hex: "13181B").opacity(0.7)) + .cornerRadius(16) + .frame(width: screenSize().width - 48) - Image("ic_seemore_vertical_white") - .resizable() - .frame(width: 24, height: 24) - .onTapGesture {} - } - .padding(.horizontal, 16) - .padding(.vertical, 8) - .frame(width: screenSize().width, height: 60) - - HStack(spacing: 8) { - Image(systemName: "info.circle.fill") - .resizable() - .frame(width: 20, height: 20) - - Text( - viewModel.characterType == .Character - ? "보이스온 AI캐릭터톡은 대화의 자유도가 높아 대화에 참여하는 당신은 누구든 될 수 있습니다.\n세계관 속 캐릭터로 대화를 하거나 새로운 인물로 캐릭터와 당신만의 스토리를 만들어보세요.\n※ AI캐릭터톡은 오픈베타 서비스 중이며, 캐릭터의 대화가 어색하거나 불완전할 수 있습니다." - : "AI Clone은 크리에이터의 정보를 기반으로 대화하지만, 모든 정보를 완벽하게 반영하거나 실제 대화와 일치하지 않을 수 있습니다." - ) - .font(.custom(Font.preRegular.rawValue, size: 12)) - .foregroundColor(.white) - - Image(systemName: "chevron.up") - .resizable() - .scaledToFit() - .frame(width: 20) - } - .padding(12) - .background(Color(hex: "13181B").opacity(0.7)) - .cornerRadius(16) - .frame(width: screenSize().width - 48) - - GeometryReader { geometry in - ScrollViewReader { proxy in - ScrollView(.vertical, showsIndicators: false) { - LazyVStack(spacing: 16) { - ForEach(0...self, from: responseData) + + if let data = decoded.data, decoded.success { + self?.resetData() + self?.getMemberInfo() + self?.enterRoom(roomId: data.chatRoomId) + } else { + if let message = decoded.message { + self?.errorMessage = message + } else { + self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self?.isShowPopup = true + } + } catch { + ERROR_LOG(String(describing: error)) + self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self?.isShowPopup = true + } + } + .store(in: &subscription) + } + func showImageViewer(_ imageUrl: String?) { if let imageUrl = imageUrl { selectedImageIndex = ownedImageUrls.firstIndex(of: imageUrl) ?? 0 @@ -298,6 +348,30 @@ final class ChatRoomViewModel: ObservableObject { } } + private func resetData() { + characterProfileUrl = "" + characterName = "Character Name" + characterType = .Character + chatRoomBgImageUrl = nil + roomId = 0 + + countdownText = "00:00:00" + showQuotaNoticeView = false + + showSendingMessage = false + + messageText = "" + messages = [] + selectedMessage = nil + selectedMessageIndex = -1 + isShowImageViewer = false + selectedImageIndex = 0 + + isShowingChatSettingsView = false + isShowingChangeBgView = false + isShowingChatResetConfirmDialog = false + } + private func checkQuotaStatus() { isLoading = true @@ -420,4 +494,8 @@ final class ChatRoomViewModel: ObservableObject { private func bgImageIdKey() -> String { return "chat_bg_image_id_room_\(roomId)" } + + private func bgHideKey() -> String { + return "chat_bg_hide_room_\(roomId)" + } } diff --git a/SodaLive/Sources/Chat/Talk/Room/Settings/ChatSettingsView.swift b/SodaLive/Sources/Chat/Talk/Room/Settings/ChatSettingsView.swift index 5be04f2..3fde9ec 100644 --- a/SodaLive/Sources/Chat/Talk/Room/Settings/ChatSettingsView.swift +++ b/SodaLive/Sources/Chat/Talk/Room/Settings/ChatSettingsView.swift @@ -8,11 +8,111 @@ import SwiftUI struct ChatSettingsView: View { + + @Binding var isShowing: Bool + @Binding var isHideBg: Bool + + let onTapChangeBg: () -> Void + let onTapResetChatRoom: () -> Void + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + VStack(spacing: 0) { + DetailNavigationBar(title: "대화 설정") { + isShowing = false + } + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + VStack(spacing: 0) { + Toggle(isOn: $isHideBg) { + Text("배경 이미지 숨김") + .font(.custom(Font.preBold.rawValue, size: 18)) + .foregroundColor(Color(hex: "B0BEC5")) + } + .toggleStyle(.switch) + .tint(Color.button) + .padding(.horizontal, 24) + .padding(.vertical, 12) + + Rectangle() + .foregroundColor(Color.white.opacity(0.14)) + .frame(maxWidth: .infinity) + .frame(height: 1) + } + + VStack(spacing: 0) { + HStack { + Text("배경 이미지 변경") + .font(.custom(Font.preBold.rawValue, size: 18)) + .foregroundColor(Color(hex: "B0BEC5")) + .padding(.horizontal, 24) + .padding(.vertical, 12) + + Spacer() + } + + Rectangle() + .foregroundColor(Color.white.opacity(0.14)) + .frame(maxWidth: .infinity) + .frame(height: 1) + } + .onTapGesture { onTapChangeBg() } + + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 6) { + Text("대화 초기화") + .font(.custom(Font.preBold.rawValue, size: 18)) + .foregroundColor(Color(hex: "B0BEC5")) + + HStack(alignment: .top, spacing: 0) { + Text("⚠️ ") + .font(.custom(Font.preRegular.rawValue, size: 16)) + .foregroundColor(.white.opacity(0.7)) + + Text("지금까지의 대화가 모두 초기화 되고, 이용자가 새로운 캐릭터가 되어 새롭게 대화를 시작합니다.") + .font(.custom(Font.preRegular.rawValue, size: 16)) + .foregroundColor(.white.opacity(0.7)) + .fixedSize(horizontal: false, vertical: true) + } + } + + Spacer() + + HStack(spacing: 4) { + Image("ic_can") + .resizable() + .frame(width: 24, height: 24) + + Text("30") + .font(.custom(Font.preBold.rawValue, size: 16)) + .foregroundColor(Color(hex: "263238")) + } + .padding(.vertical, 3) + .padding(.horizontal, 10) + .background(Color(hex: "B5E7FA")) + .cornerRadius(30) + .overlay { + RoundedRectangle(cornerRadius: 30) + .stroke(lineWidth: 1) + .foregroundColor(.button) + } + + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .onTapGesture { onTapResetChatRoom() } + } + } + } + .background(Color.black) } } #Preview { - ChatSettingsView() + ChatSettingsView( + isShowing: .constant(true), + isHideBg: .constant(false), + onTapChangeBg: {}, + onTapResetChatRoom: {} + ) } diff --git a/SodaLive/Sources/Chat/Talk/TalkApi.swift b/SodaLive/Sources/Chat/Talk/TalkApi.swift index 914e30b..1b247de 100644 --- a/SodaLive/Sources/Chat/Talk/TalkApi.swift +++ b/SodaLive/Sources/Chat/Talk/TalkApi.swift @@ -19,6 +19,7 @@ enum TalkApi { case purchaseChatQuota(request: ChatQuotaPurchaseRequest) case purchaseMessage(roomId: Int, messageId: Int64, request: ChatMessagePurchaseRequest) + case resetChatRoom(roomId: Int, request: ChatRoomResetRequest) } extension TalkApi: TargetType { @@ -49,6 +50,9 @@ extension TalkApi: TargetType { case .purchaseMessage(let roomId, let messageId, _): return "/api/chat/room/\(roomId)/messages/\(messageId)/purchase" + + case .resetChatRoom(let roomId, _): + return "/api/chat/room/\(roomId)/reset" } } @@ -77,6 +81,9 @@ extension TalkApi: TargetType { case .purchaseMessage: return .post + + case .resetChatRoom: + return .post } } @@ -123,6 +130,9 @@ extension TalkApi: TargetType { case .purchaseMessage(_, _, let request): return .requestJSONEncodable(request) + + case .resetChatRoom(_, let request): + return .requestJSONEncodable(request) } }