// // AuditionRoleDetailView.swift // SodaLive // // Created by klaus on 1/6/25. // import SwiftUI import Kingfisher struct AuditionRoleDetailView: View { let roleId: Int let auditionTitle: String @StateObject var viewModel = AuditionRoleDetailViewModel() @StateObject var keyboardHandler = KeyboardHandler() @StateObject var soundManager = AuditionSoundManager.shared @State private var isShowApplyMethodView = false @State private var isShowSelectAudioView = false @State private var isShowRecordingView = false @State private var isShowNoticeAuthView = false @State private var isShowApplyView = false @State private var isShowNoticeReapply = false @State private var isShowApplyCompleteView = false var body: some View { BaseView(isLoading: $viewModel.isLoading) { ZStack(alignment: .bottomTrailing) { VStack(spacing: 0) { DetailNavigationBar(title: viewModel.name) ScrollView(.vertical, showsIndicators: false) { LazyVStack(spacing: 15) { if let roleDetail = viewModel.auditionRoleDetail { KFImage(URL(string: roleDetail.imageUrl)) .cancelOnDisappear(true) .downsampling(size: CGSize(width: 1000, height: 350)) .resizable() .aspectRatio(1000/350, contentMode: .fit) .frame(maxWidth: .infinity) .cornerRadius(6.7) .padding(.top, 3) HStack(spacing: 14) { if let url = URL(string: roleDetail.originalWorkUrl), UIApplication.shared.canOpenURL(url) { Text("원작 보러가기") .font(.custom(Font.bold.rawValue, size: 16)) .foregroundColor(Color.button) .padding(.vertical, 12) .frame(maxWidth: .infinity) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.button, lineWidth: 1) ) .contentShape(Rectangle()) .onTapGesture { UIApplication.shared.open(url) } } if let url = URL(string: roleDetail.auditionScriptUrl), UIApplication.shared.canOpenURL(url) { Text("오디션 대본 확인") .font(.custom(Font.bold.rawValue, size: 16)) .foregroundColor(Color.button) .padding(.vertical, 12) .frame(maxWidth: .infinity) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.button, lineWidth: 1) ) .contentShape(Rectangle()) .onTapGesture { UIApplication.shared.open(url) } } } VStack(alignment: .leading, spacing: 13.3) { Text("오디션 캐릭터 정보") .font(.custom(Font.bold.rawValue, size: 14.7)) .foregroundColor(Color.grayee) ExpandableTextView(text: roleDetail.information) } } if viewModel.applicantList.isEmpty { Text("지원자가 없습니다.") .font(.custom(Font.medium.rawValue, size: 13)) .foregroundColor(Color.grayee) .padding(.top, 15) } else { HStack(spacing: 0) { Text("참여자") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color.graybb) Text("\(viewModel.totalCount)") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color.button) .padding(.leading, 2.3) Text("명") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor(Color.graybb) Spacer() Text("최신순") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor( viewModel.sortType == .NEWEST ? Color.button : Color.graybb ) .onTapGesture { viewModel.setSortType(sortType: .NEWEST) } Text("좋아요순") .font(.custom(Font.medium.rawValue, size: 13.3)) .foregroundColor( viewModel.sortType == .LIKES ? Color.button : Color.graybb ) .onTapGesture { viewModel.setSortType(sortType: .LIKES) } .padding(.leading, 13.3) } .padding(.top, 15) VStack(spacing: 5.3) { ForEach(0..) { switch result { case .success(let url): let fileUrl = url[0] if fileUrl.startAccessingSecurityScopedResource() { defer { fileUrl.stopAccessingSecurityScopedResource() } if let data = try? Data(contentsOf: fileUrl) { viewModel.soundData = data viewModel.fileName = fileUrl.lastPathComponent isShowApplyView = true } else { viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" viewModel.isShowPopup = true } } else { viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" viewModel.isShowPopup = true } case .failure(let error): DEBUG_LOG("error: \(error.localizedDescription)") viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" viewModel.isShowPopup = true } } } #Preview { AuditionRoleDetailView(roleId: 1, auditionTitle: "스위치온") }