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
 | 
			
		||||
 | 
			
		||||
struct TalkView: View {
 | 
			
		||||
  var body: some View {
 | 
			
		||||
    VStack(spacing: 12) {
 | 
			
		||||
      Spacer()
 | 
			
		||||
      Text("톡 페이지 (준비중)")
 | 
			
		||||
        .font(.custom(Font.preMedium.rawValue, size: 16))
 | 
			
		||||
        .multilineTextAlignment(.center)
 | 
			
		||||
      Spacer()
 | 
			
		||||
    
 | 
			
		||||
    @StateObject var viewModel = TalkViewModel()
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        BaseView(isLoading: $viewModel.isLoading) {
 | 
			
		||||
            if viewModel.talkRooms.isEmpty {
 | 
			
		||||
                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 {
 | 
			
		||||
  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