feat(chat-talk): 톡 목록 조회 API 연동 및 목록 UI 구성
This commit is contained in:
		
							
								
								
									
										42
									
								
								SodaLive/Sources/Chat/Talk/TalkApi.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								SodaLive/Sources/Chat/Talk/TalkApi.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  TalkApi.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 8/29/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Foundation
 | 
				
			||||||
 | 
					import Moya
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum TalkApi {
 | 
				
			||||||
 | 
					    case getTalkRooms
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extension TalkApi: TargetType {
 | 
				
			||||||
 | 
					    var baseURL: URL { URL(string: BASE_URL)! }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var path: String {
 | 
				
			||||||
 | 
					        switch self {
 | 
				
			||||||
 | 
					        case .getTalkRooms:
 | 
				
			||||||
 | 
					            return "/api/chat/room/list"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var method: Moya.Method {
 | 
				
			||||||
 | 
					        switch self {
 | 
				
			||||||
 | 
					        case .getTalkRooms:
 | 
				
			||||||
 | 
					            return .get
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var task: Moya.Task {
 | 
				
			||||||
 | 
					        switch self {
 | 
				
			||||||
 | 
					        case .getTalkRooms:
 | 
				
			||||||
 | 
					            return .requestPlain
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var headers: [String : String]? {
 | 
				
			||||||
 | 
					        ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										87
									
								
								SodaLive/Sources/Chat/Talk/TalkItemView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								SodaLive/Sources/Chat/Talk/TalkItemView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  TalkItemView.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 8/29/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					import Kingfisher
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct TalkItemView: View {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let item: TalkRoom
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @State private var backgroundColor = Color(hex: "009D68")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var body: some View {
 | 
				
			||||||
 | 
					        HStack(spacing: 13) {
 | 
				
			||||||
 | 
					            KFImage(URL(string: item.imageUrl))
 | 
				
			||||||
 | 
					                .placeholder { Circle().fill(Color.gray.opacity(0.2)) }
 | 
				
			||||||
 | 
					                .retry(maxCount: 2, interval: .seconds(1))
 | 
				
			||||||
 | 
					                .cancelOnDisappear(true)
 | 
				
			||||||
 | 
					                .resizable()
 | 
				
			||||||
 | 
					                .scaledToFill()
 | 
				
			||||||
 | 
					                .frame(width: 76, height: 76)
 | 
				
			||||||
 | 
					                .clipShape(Circle())
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            VStack(alignment: .leading, spacing: 6) {
 | 
				
			||||||
 | 
					                HStack(spacing: 4) {
 | 
				
			||||||
 | 
					                    Text(item.title)
 | 
				
			||||||
 | 
					                        .font(.custom(Font.preBold.rawValue, size: 18))
 | 
				
			||||||
 | 
					                        .foregroundColor(.white)
 | 
				
			||||||
 | 
					                        .lineLimit(1)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Text(item.opponentType)
 | 
				
			||||||
 | 
					                        .font(.custom(Font.preRegular.rawValue, size: 12))
 | 
				
			||||||
 | 
					                        .foregroundColor(Color(hex: "D9FCF4"))
 | 
				
			||||||
 | 
					                        .lineLimit(1)
 | 
				
			||||||
 | 
					                        .padding(.horizontal, 5)
 | 
				
			||||||
 | 
					                        .padding(.vertical, 1)
 | 
				
			||||||
 | 
					                        .background(backgroundColor)
 | 
				
			||||||
 | 
					                        .cornerRadius(6)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Spacer()
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Text(item.lastMessageTimeLabel)
 | 
				
			||||||
 | 
					                        .font(.custom(Font.preRegular.rawValue, size: 12))
 | 
				
			||||||
 | 
					                        .foregroundColor(Color(hex: "78909C"))
 | 
				
			||||||
 | 
					                        .lineLimit(1)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if let message = item.lastMessagePreview {
 | 
				
			||||||
 | 
					                    Text(message)
 | 
				
			||||||
 | 
					                        .font(.custom(Font.preRegular.rawValue, size: 14))
 | 
				
			||||||
 | 
					                        .foregroundColor(Color(hex: "b0bec5"))
 | 
				
			||||||
 | 
					                        .lineLimit(2)
 | 
				
			||||||
 | 
					                        .truncationMode(.tail)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .onAppear {
 | 
				
			||||||
 | 
					            switch item.opponentType.lowercased() {
 | 
				
			||||||
 | 
					            case "clone":
 | 
				
			||||||
 | 
					                self.backgroundColor = Color(hex: "0020C9")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            case "creator":
 | 
				
			||||||
 | 
					                self.backgroundColor = Color(hex: "F86660")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                self.backgroundColor = Color(hex: "009D68")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#Preview {
 | 
				
			||||||
 | 
					    TalkItemView(
 | 
				
			||||||
 | 
					        item: TalkRoom(
 | 
				
			||||||
 | 
					            chatRoomId: 1,
 | 
				
			||||||
 | 
					            title: "정인이",
 | 
				
			||||||
 | 
					            imageUrl: "https://picsum.photos/200",
 | 
				
			||||||
 | 
					            opponentType: "Character",
 | 
				
			||||||
 | 
					            lastMessagePreview: "태풍온다잖아 ㅜㅜ\n조심해 어디 나가지 말고 집에만...",
 | 
				
			||||||
 | 
					            lastMessageTimeLabel: "6월 15일"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								SodaLive/Sources/Chat/Talk/TalkRepository.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								SodaLive/Sources/Chat/Talk/TalkRepository.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  TalkRepository.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 8/29/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Foundation
 | 
				
			||||||
 | 
					import CombineMoya
 | 
				
			||||||
 | 
					import Combine
 | 
				
			||||||
 | 
					import Moya
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TalkRepository {
 | 
				
			||||||
 | 
					    private let api = MoyaProvider<TalkApi>()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    func getTalkRooms() -> AnyPublisher<Response, MoyaError> {
 | 
				
			||||||
 | 
					        return api.requestPublisher(.getTalkRooms)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								SodaLive/Sources/Chat/Talk/TalkRoom.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								SodaLive/Sources/Chat/Talk/TalkRoom.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  TalkRoom.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 8/29/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct TalkRoom: Decodable {
 | 
				
			||||||
 | 
					    let chatRoomId: Int
 | 
				
			||||||
 | 
					    let title: String
 | 
				
			||||||
 | 
					    let imageUrl: String
 | 
				
			||||||
 | 
					    let opponentType: String
 | 
				
			||||||
 | 
					    let lastMessagePreview: String?
 | 
				
			||||||
 | 
					    let lastMessageTimeLabel: String
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,17 +8,33 @@
 | 
				
			|||||||
import SwiftUI
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct TalkView: View {
 | 
					struct TalkView: View {
 | 
				
			||||||
  var body: some View {
 | 
					    
 | 
				
			||||||
    VStack(spacing: 12) {
 | 
					    @StateObject var viewModel = TalkViewModel()
 | 
				
			||||||
      Spacer()
 | 
					    
 | 
				
			||||||
      Text("톡 페이지 (준비중)")
 | 
					    var body: some View {
 | 
				
			||||||
        .font(.custom(Font.preMedium.rawValue, size: 16))
 | 
					        BaseView(isLoading: $viewModel.isLoading) {
 | 
				
			||||||
        .multilineTextAlignment(.center)
 | 
					            if viewModel.talkRooms.isEmpty {
 | 
				
			||||||
      Spacer()
 | 
					                Text("대화 중인 톡이 없습니다")
 | 
				
			||||||
 | 
					                    .font(.custom(Font.preRegular.rawValue, size: 20))
 | 
				
			||||||
 | 
					                    .foregroundColor(.white)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                ScrollView(.vertical, showsIndicators: false) {
 | 
				
			||||||
 | 
					                    VStack(spacing: 24) {
 | 
				
			||||||
 | 
					                        ForEach(0..<viewModel.talkRooms.count, id: \.self) {
 | 
				
			||||||
 | 
					                            TalkItemView(item: viewModel.talkRooms[$0])
 | 
				
			||||||
 | 
					                                .padding(.horizontal, 24)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .padding(.vertical, 24)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .onAppear {
 | 
				
			||||||
 | 
					            viewModel.getTalkRooms()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#Preview {
 | 
					#Preview {
 | 
				
			||||||
  TalkView()
 | 
					    TalkView()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										63
									
								
								SodaLive/Sources/Chat/Talk/TalkViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								SodaLive/Sources/Chat/Talk/TalkViewModel.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  TalkViewModel.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 8/29/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Foundation
 | 
				
			||||||
 | 
					import Combine
 | 
				
			||||||
 | 
					import Moya
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class TalkViewModel: ObservableObject {
 | 
				
			||||||
 | 
					    // MARK: - Published State
 | 
				
			||||||
 | 
					    @Published private(set) var talkRooms = [TalkRoom]()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Published var isLoading: Bool = false
 | 
				
			||||||
 | 
					    @Published var errorMessage: String = ""
 | 
				
			||||||
 | 
					    @Published var isShowPopup = false
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // MARK: - Private
 | 
				
			||||||
 | 
					    private let repository = TalkRepository()
 | 
				
			||||||
 | 
					    private var subscription = Set<AnyCancellable>()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // MARK: - API
 | 
				
			||||||
 | 
					    func getTalkRooms() {
 | 
				
			||||||
 | 
					        isLoading = true
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        repository.getTalkRooms()
 | 
				
			||||||
 | 
					            .sink { result in
 | 
				
			||||||
 | 
					                switch result {
 | 
				
			||||||
 | 
					                case .finished:
 | 
				
			||||||
 | 
					                    DEBUG_LOG("finish")
 | 
				
			||||||
 | 
					                case .failure(let error):
 | 
				
			||||||
 | 
					                    ERROR_LOG(error.localizedDescription)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } receiveValue: { response in
 | 
				
			||||||
 | 
					                let responseData = response.data
 | 
				
			||||||
 | 
					                self.isLoading = false
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                do {
 | 
				
			||||||
 | 
					                    let jsonDecoder = JSONDecoder()
 | 
				
			||||||
 | 
					                    let decoded = try jsonDecoder.decode(ApiResponse<[TalkRoom]>.self, from: responseData)
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if let data = decoded.data, decoded.success {
 | 
				
			||||||
 | 
					                        self.talkRooms = data
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        if let message = decoded.message {
 | 
				
			||||||
 | 
					                            self.errorMessage = message
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        self.isShowPopup = true
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } catch {
 | 
				
			||||||
 | 
					                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
				
			||||||
 | 
					                    self.isShowPopup = true
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .store(in: &subscription)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Reference in New Issue
	
	Block a user