Files
sodalive-ios/SodaLive/Sources/Live/Room/V2V/V2vRepository.swift
Yu Sung b796f6d9c5 라이브룸 V2V 번역 자막 기능을 추가한다
라이브룸에서 진행자 언어와 기기 언어가 다를 때 자막 토글을 제공한다.
룸 정보 응답에 V2V 워커 토큰과 진행자 언어 코드를 포함한다.
Agora V2V 에이전트 참여와 종료 API 연동을 추가한다
2026-02-09 21:11:17 +09:00

129 lines
4.1 KiB
Swift

//
// V2vRepository.swift
// SodaLive
//
// Created by klaus on 2/9/26.
//
import Foundation
import Combine
import Moya
import CombineMoya
protocol V2VRepository {
func join(request: V2VJoinRequest) -> AnyPublisher<String, V2VRepositoryError>
func leave(agentId: String) -> AnyPublisher<Void, V2VRepositoryError>
}
enum V2VRepositoryError: Error {
case network(message: String)
case decoding
case business(message: String)
var userMessage: String {
switch self {
case .network(let message):
return message
case .decoding:
return I18n.Common.commonError
case .business(let message):
return message
}
}
}
final class V2VRepositoryImpl: V2VRepository {
private let api: MoyaProvider<V2vApi>
init(api: MoyaProvider<V2vApi> = MoyaProvider<V2vApi>()) {
self.api = api
}
func join(request: V2VJoinRequest) -> AnyPublisher<String, V2VRepositoryError> {
api.requestPublisher(.join(request: request))
.tryMap { response in
try Self.validateStatusCode(response)
do {
let decoded = try JSONDecoder().decode(V2VJoinResponse.self, from: response.data)
guard !decoded.agentId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw V2VRepositoryError.decoding
}
return decoded.agentId
} catch let error as V2VRepositoryError {
throw error
} catch {
throw V2VRepositoryError.decoding
}
}
.mapError { Self.mapError($0) }
.eraseToAnyPublisher()
}
func leave(agentId: String) -> AnyPublisher<Void, V2VRepositoryError> {
api.requestPublisher(.leave(agentId: agentId))
.tryMap { response in
try Self.validateStatusCode(response)
if !response.data.isEmpty {
if let text = String(data: response.data, encoding: .utf8) {
DEBUG_LOG("[V2V] leave response: \(text)")
}
_ = try? JSONDecoder().decode(V2VLeaveResponse.self, from: response.data)
}
return ()
}
.mapError { Self.mapError($0) }
.eraseToAnyPublisher()
}
private static func validateStatusCode(_ response: Response) throws {
guard (200..<300).contains(response.statusCode) else {
throw V2VRepositoryError.business(message: parseBusinessMessage(from: response.data) ?? I18n.Common.commonError)
}
}
private static func mapError(_ error: Error) -> V2VRepositoryError {
if let mapped = error as? V2VRepositoryError {
return mapped
}
guard let moyaError = error as? MoyaError else {
return .network(message: I18n.Common.commonError)
}
switch moyaError {
case .statusCode(let response):
if let message = parseBusinessMessage(from: response.data) {
return .business(message: message)
}
return .business(message: I18n.Common.commonError)
case .objectMapping, .jsonMapping, .encodableMapping, .stringMapping, .imageMapping:
return .decoding
default:
return .network(message: I18n.Common.commonError)
}
}
private static func parseBusinessMessage(from data: Data) -> String? {
if let decoded = try? JSONDecoder().decode(V2VErrorResponse.self, from: data),
let message = decoded.message?.trimmingCharacters(in: .whitespacesAndNewlines),
!message.isEmpty {
return message
}
if let decoded = try? JSONDecoder().decode(ApiResponseWithoutData.self, from: data),
let message = decoded.message?.trimmingCharacters(in: .whitespacesAndNewlines),
!message.isEmpty {
return message
}
return nil
}
}
private struct V2VErrorResponse: Decodable {
let message: String?
}