// // CharacterDetailGalleryView.swift // SodaLive // // Created by klaus on 9/1/25. // import SwiftUI struct CharacterDetailGalleryView: View { @StateObject var viewModel = CharacterDetailGalleryViewModel() private let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3) let characterId: Int // 계산된 속성들 private var progressBarWidth: CGFloat { let maxWidth: CGFloat = screenSize().width - 48 guard viewModel.totalCount > 0 else { return 0 } let percentage = Double(viewModel.ownedCount) / Double(viewModel.totalCount) return maxWidth * percentage } var body: some View { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 24) { // 보유 정보 섹션 collectionInfoView() .padding(.horizontal, 24) // 갤러리 그리드 ScrollView { LazyVGrid(columns: columns, spacing: 0) { ForEach(Array(viewModel.galleryItems.enumerated()), id: \.element.id) { index, item in galleryImageView(item: item, index: index) .onAppear { viewModel.loadMoreIfNeeded(currentItem: item) } } } .frame(width: screenSize().width) } Spacer() } .padding(.top, 24) .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { GeometryReader { geo in HStack { Spacer() Text(viewModel.errorMessage) .padding(.vertical, 13.3) .frame(alignment: .center) .frame(maxWidth: .infinity) .padding(.horizontal, 33.3) .font(.custom(Font.medium.rawValue, size: 12)) .background(Color.button) .foregroundColor(Color.white) .multilineTextAlignment(.center) .cornerRadius(20) .padding(.top, 66.7) Spacer() } } } } .onAppear { viewModel.characterId = characterId viewModel.loadInitialData() } .sheet(isPresented: $viewModel.isShowImageViewer) { ImageViewerView( images: viewModel.ownedImages.map { $0.imageUrl }, selectedIndex: $viewModel.selectedImageIndex ) } .overlay { if viewModel.isShowPurchaseDialog { SodaDialog( title: "구매 확인", desc: "선택한 이미지를 구매하시겠습니까?", confirmButtonTitle: viewModel.selectedItemPrice, confirmButtonAction: viewModel.onPurchaseConfirm, cancelButtonTitle: "취소", cancelButtonAction: viewModel.onPurchaseCancel ) } } } @ViewBuilder private func collectionInfoView() -> some View { VStack(spacing: 8) { // 상단 정보 (계산된 % 보유중, 정보 아이콘, 개수) HStack { Text("\(viewModel.ownershipPercentage)% 보유중") .font(.custom(Font.preBold.rawValue, size: 18)) .foregroundColor(.white) Spacer() HStack(spacing: 4) { Text("\(viewModel.ownedCount)") .font(.custom(Font.preRegular.rawValue, size: 16)) .foregroundColor(Color(hex: "#FDD453")) Text("/") .font(.custom(Font.preRegular.rawValue, size: 16)) .foregroundColor(.white) Text("\(viewModel.totalCount)개") .font(.custom(Font.preRegular.rawValue, size: 16)) .foregroundColor(.white) } } // 진행률 바 GeometryReader { geometry in ZStack(alignment: .leading) { // 배경 바 RoundedRectangle(cornerRadius: 999) .foregroundColor(Color(hex: "#37474F")) .frame(height: 9) // 진행률 바 (계산된 퍼센트) RoundedRectangle(cornerRadius: 999) .fill( LinearGradient( colors: [Color(hex: "#80D8FF"), Color(hex: "#6D5ED7")], startPoint: .leading, endPoint: .trailing ) ) .frame(width: min(progressBarWidth, geometry.size.width), height: 9) } } .frame(height: 9) } } @ViewBuilder private func galleryImageView(item: CharacterImageListItemResponse, index: Int) -> some View { ZStack { // 이미지 AsyncImage(url: URL(string: item.imageUrl)) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Rectangle() .fill(Color.gray.opacity(0.3)) } .frame(width: 132, height: 165) .clipped() .cornerRadius(0) // 미소유 이미지 오버레이 if !item.isOwned { // 어두운 오버레이 Rectangle() .fill(Color.black.opacity(0.2)) .frame(width: 132, height: 165) // 자물쇠 아이콘과 코인 정보 VStack(spacing: 8) { // 자물쇠 아이콘 Image("ic_new_lock") .resizable() .scaledToFit() .frame(width: 24) // 코인 정보 배경 HStack(spacing: 4) { Image("ic_can") .resizable() .scaledToFit() .frame(width: 16) Text("\(item.imagePriceCan)") .font(.custom(Font.preBold.rawValue, size: 16)) .foregroundColor(Color(hex: "#263238")) } .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color(hex: "#B5E7FA")) .cornerRadius(30) .overlay { RoundedRectangle(cornerRadius: 30) .strokeBorder(lineWidth: 1) .foregroundColor(.button) } } } } .onTapGesture { viewModel.onImageTapped(item, index: index) } } } #Preview { CharacterDetailGalleryView(characterId: 1) }