211 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			211 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
//
 | 
						|
//  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 {
 | 
						|
        GeometryReader { geo in
 | 
						|
            let width = geo.size.width
 | 
						|
            let height = width * 5 / 4
 | 
						|
            
 | 
						|
            ZStack {
 | 
						|
                // 이미지
 | 
						|
                AsyncImage(url: URL(string: item.imageUrl)) { image in
 | 
						|
                    image
 | 
						|
                        .resizable()
 | 
						|
                        .scaledToFill()
 | 
						|
                        .frame(width: width, height: height)
 | 
						|
                        .clipped()
 | 
						|
                } placeholder: {
 | 
						|
                    Rectangle()
 | 
						|
                        .fill(Color.gray.opacity(0.3))
 | 
						|
                }
 | 
						|
                
 | 
						|
                // 미소유 이미지 오버레이
 | 
						|
                if !item.isOwned {
 | 
						|
                    // 어두운 오버레이
 | 
						|
                    Rectangle()
 | 
						|
                        .fill(Color.black.opacity(0.2))
 | 
						|
                    
 | 
						|
                    // 자물쇠 아이콘과 코인 정보
 | 
						|
                    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)
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .frame(width: width, height: height)
 | 
						|
            .clipped()
 | 
						|
            .contentShape(Rectangle())
 | 
						|
            .onTapGesture {
 | 
						|
                viewModel.onImageTapped(item, index: index)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        .aspectRatio(4/5, contentMode: .fit)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#Preview {
 | 
						|
    CharacterDetailGalleryView(characterId: 1)
 | 
						|
}
 |