diff --git a/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailHeaderView.swift b/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailHeaderView.swift index f8565a1..2d2628e 100644 --- a/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailHeaderView.swift +++ b/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailHeaderView.swift @@ -12,8 +12,6 @@ struct OriginalWorkDetailHeaderView: View { let item: OriginalWorkDetailResponse - @State var isDescriptionExpanded = false - var body: some View { VStack(spacing: 0) { if let imageUrl = item.imageUrl { @@ -74,33 +72,15 @@ struct OriginalWorkDetailHeaderView: View { } .padding(.top, 14) - Text(item.description) - .font(.custom(Font.preRegular.rawValue, size: 14)) - .foregroundColor(Color(hex: "cfd8dc")) - .lineLimit(!isDescriptionExpanded ? 2 : Int.max) - .truncationMode(.tail) - .frame(maxWidth: .infinity) - .padding(.top, 14) - .onTapGesture { - isDescriptionExpanded.toggle() - } - - Text("원작 보러가기") - .font(.custom(Font.preBold.rawValue, size: 16)) - .foregroundColor(.button) - .frame(maxWidth: .infinity) - .padding(.vertical, 15) - .overlay { - RoundedRectangle(cornerRadius: 8) - .strokeBorder(lineWidth: 1) - .foregroundColor(.button) - } - .padding(.top, 24) - .onTapGesture { - if let link = item.originalLink, let url = URL(string: link) { - UIApplication.shared.open(url) - } - } + Text( + item.tags + .map { $0.hasPrefix("#") ? $0 : "#\($0)" } + .joined(separator: " ") + ) + .font(.custom(Font.preRegular.rawValue, size: 14)) + .foregroundColor(Color(hex: "3bb9f1")) + .frame(maxWidth: .infinity) + .padding(.top, 14) } } } @@ -114,7 +94,12 @@ struct OriginalWorkDetailHeaderView: View { category: "로맨스", isAdult: true, description: "작품설명입니다.보이스온의 오픈월드 캐릭터톡은 청소년 보호를 위해 본인인증한성인만 이용이 가능합니다.캐릭터톡 서비스를 이용하시려면 본인인증을 하고 이용해주세요.", + originalWork: nil, originalLink: "https://apple.com", + writer: nil, + studio: nil, + originalLinks: [], + tags: [], characters: [] ) ) diff --git a/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailResponse.swift b/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailResponse.swift index 3e5abbd..3a1e4f0 100644 --- a/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailResponse.swift +++ b/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailResponse.swift @@ -12,6 +12,11 @@ struct OriginalWorkDetailResponse: Decodable { let category: String let isAdult: Bool let description: String + let originalWork: String? let originalLink: String? + let writer: String? + let studio: String? + let originalLinks: [String] + let tags: [String] let characters: [Character] } diff --git a/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailView.swift b/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailView.swift index 6b98af2..219e155 100644 --- a/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailView.swift +++ b/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailView.swift @@ -14,9 +14,6 @@ struct OriginalWorkDetailView: View { let originalId: Int - private let horizontalPadding: CGFloat = 12 - private let gridSpacing: CGFloat = 12 - var body: some View { NavigationStack { BaseView(isLoading: $viewModel.isLoading) { @@ -46,54 +43,46 @@ struct OriginalWorkDetailView: View { .frame(height: 56) if let response = viewModel.response { - GeometryReader { geo in - let totalSpacing: CGFloat = gridSpacing * 2 - let width = (geo.size.width - (horizontalPadding * 2) - totalSpacing) / 3 - - ScrollView(.vertical, showsIndicators: false) { - VStack(spacing: 0) { - OriginalWorkDetailHeaderView(item: response) - .padding(.horizontal, 24) - .padding(.bottom, 24) - - LazyVGrid( - columns: Array( - repeating: GridItem( - .flexible(), - spacing: gridSpacing, - alignment: .topLeading - ), - count: 3 - ), - alignment: .leading, - spacing: gridSpacing + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + OriginalWorkDetailHeaderView(item: response) + .padding(.horizontal, 24) + .padding(.bottom, 24) + + HStack(spacing: 0) { + SeriesDetailTabView( + title: "캐릭터", + width: screenSize().width / 2, + isSelected: viewModel.currentTab == .character ) { - ForEach(viewModel.characters.indices, id: \.self) { idx in - let item = viewModel.characters[idx] - - NavigationLink(value: item.characterId) { - CharacterItemView( - character: item, - size: width, - rank: 0, - isShowRank: false - ) - .onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) } - } + if viewModel.currentTab != .character { + viewModel.currentTab = .character } } - .padding(.horizontal, horizontalPadding) - if viewModel.isLoadingMore { - HStack { - Spacer() - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .padding(.vertical, 16) - Spacer() + SeriesDetailTabView( + title: "작품정보", + width: screenSize().width / 2, + isSelected: viewModel.currentTab == .info + ) { + if viewModel.currentTab != .info { + viewModel.currentTab = .info } } } + .background(Color.black) + + Rectangle() + .foregroundColor(Color.gray90.opacity(0.5)) + .frame(height: 1) + .frame(maxWidth: .infinity) + + switch(viewModel.currentTab) { + case .info: + OriginalWorkInfoView(response: response) + default: + OriginalWorkCharacterView(characters: viewModel.characters) + } } } } @@ -112,6 +101,170 @@ struct OriginalWorkDetailView: View { } } +struct OriginalWorkCharacterView: View { + + private let horizontalPadding: CGFloat = 12 + private let gridSpacing: CGFloat = 12 + + let characters: [Character] + + var body: some View { + ZStack { + let totalSpacing: CGFloat = gridSpacing * 2 + let width = (screenSize().width - (horizontalPadding * 2) - totalSpacing) / 3 + + LazyVGrid( + columns: Array( + repeating: GridItem( + .flexible(), + spacing: gridSpacing, + alignment: .topLeading + ), + count: 3 + ), + alignment: .leading, + spacing: gridSpacing + ) { + ForEach(characters.indices, id: \.self) { idx in + let item = characters[idx] + + NavigationLink(value: item.characterId) { + CharacterItemView( + character: item, + size: width, + rank: 0, + isShowRank: false + ) + } + } + } + .padding(.horizontal, horizontalPadding) + } + .padding(.top, 24) + .background(Color.black) + } +} + +struct OriginalWorkInfoView: View { + + let response: OriginalWorkDetailResponse + + @State private var isExpandDesc = false + + var body: some View { + ZStack { + VStack(spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("작품 소개") + .font(.custom(Font.preBold.rawValue, size: 16)) + .foregroundColor(.white) + + Text(response.description) + .font(.custom(Font.preRegular.rawValue, size: 14)) + .foregroundColor(Color(hex: "B0BEC5")) + .lineLimit(isExpandDesc ? Int.max : 3) + .truncationMode(.tail) + .onTapGesture { + isExpandDesc.toggle() + } + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(hex: "263238")) + .cornerRadius(16) + + VStack(alignment: .leading, spacing: 8) { + Text("원작 보러 가기") + .font(.custom(Font.preBold.rawValue, size: 16)) + .foregroundColor(Color(hex: "B0BEC5")) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(0..() - private var currentPage: Int = 0 - private var hasMorePages: Bool = true var originalId: Int = 0 { didSet { @@ -31,14 +33,6 @@ final class OriginalWorkDetailViewModel: ObservableObject { } // MARK: - API - func loadMoreIfNeeded(currentIndex: Int) { - guard hasMorePages, - !isLoading, - !isLoadingMore, - currentIndex >= characters.count - 3 else { return } - fetchCharacters() - } - private func fetchDetail() { isLoading = true @@ -80,53 +74,4 @@ final class OriginalWorkDetailViewModel: ObservableObject { } .store(in: &subscription) } - - private func fetchCharacters() { - currentPage += 1 - isLoadingMore = true - - repository.getOriginalWorkCharacters(id: originalId, page: currentPage) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - switch completion { - case .finished: - DEBUG_LOG("finish") - case .failure(let error): - ERROR_LOG(error.localizedDescription) - self?.isLoadingMore = false - self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." - self?.isShowPopup = true - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - do { - let jsonDecoder = JSONDecoder() - let decoded = try jsonDecoder.decode(ApiResponse.self, from: response.data) - if let data = decoded.data, decoded.success { - self.totalCount = data.totalCount - - if !data.content.isEmpty { - self.characters.append(contentsOf: data.content) - } else { - hasMorePages = false - } - - self.isLoadingMore = false - } else { - if let message = decoded.message { - self.errorMessage = message - } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." - } - self.isShowPopup = true - self.isLoadingMore = false - } - } catch { - self.isLoading = false - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." - self.isShowPopup = true - } - } - .store(in: &subscription) - } } diff --git a/SodaLive/Sources/Chat/Original/OriginalWorkApi.swift b/SodaLive/Sources/Chat/Original/OriginalWorkApi.swift index 45539e5..6c531d3 100644 --- a/SodaLive/Sources/Chat/Original/OriginalWorkApi.swift +++ b/SodaLive/Sources/Chat/Original/OriginalWorkApi.swift @@ -11,7 +11,6 @@ import Moya enum OriginalWorkApi { case getOriginalWorks(page: Int, size: Int) case getOriginalDetail(id: Int) - case getOriginalWorkCharacters(id: Int, page: Int, size: Int) } extension OriginalWorkApi: TargetType { @@ -24,9 +23,6 @@ extension OriginalWorkApi: TargetType { case .getOriginalDetail(let id): return "/api/chat/original/\(id)" - - case .getOriginalWorkCharacters(let id, _, _): - return "/api/chat/original/\(id)/characters" } } @@ -44,12 +40,6 @@ extension OriginalWorkApi: TargetType { case .getOriginalDetail: return .requestPlain - - case .getOriginalWorkCharacters(_, let page, let size): - return .requestParameters( - parameters: ["page": page, "size": size], - encoding: URLEncoding.queryString - ) } } diff --git a/SodaLive/Sources/Chat/Original/OriginalWorkRepository.swift b/SodaLive/Sources/Chat/Original/OriginalWorkRepository.swift index 8669c09..066ab50 100644 --- a/SodaLive/Sources/Chat/Original/OriginalWorkRepository.swift +++ b/SodaLive/Sources/Chat/Original/OriginalWorkRepository.swift @@ -20,8 +20,4 @@ class OriginalWorkRepository { func getOriginalDetail(id: Int) -> AnyPublisher { return api.requestPublisher(.getOriginalDetail(id: id)) } - - func getOriginalWorkCharacters(id: Int, page: Int) -> AnyPublisher { - return api.requestPublisher(.getOriginalWorkCharacters(id: id, page: page, size: 20)) - } } diff --git a/SodaLive/Sources/Content/Player/ContentPlayerPlayManager.swift b/SodaLive/Sources/Content/Player/ContentPlayerPlayManager.swift index 4f80e76..eaa9db4 100644 --- a/SodaLive/Sources/Content/Player/ContentPlayerPlayManager.swift +++ b/SodaLive/Sources/Content/Player/ContentPlayerPlayManager.swift @@ -11,7 +11,7 @@ import MediaPlayer import Combine import Kingfisher -import SwiftUICore +import SwiftUI final class ContentPlayerPlayManager: NSObject, ObservableObject { enum LoopState {