diff --git a/SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/Contents.json new file mode 100644 index 0000000..8d9a045 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_new_lock.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/ic_new_lock.png b/SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/ic_new_lock.png new file mode 100644 index 0000000..a6a13de Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/ic_new_lock.png differ diff --git a/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryRepository.swift b/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryRepository.swift new file mode 100644 index 0000000..754278b --- /dev/null +++ b/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryRepository.swift @@ -0,0 +1,15 @@ +// +// CharacterDetailGalleryRepository.swift +// SodaLive +// +// Created by klaus on 9/2/25. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +final class CharacterDetailGalleryRepository { + private let characterApi = MoyaProvider() +} diff --git a/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryView.swift b/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryView.swift index 3786a26..03dd41f 100644 --- a/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryView.swift +++ b/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryView.swift @@ -8,8 +8,157 @@ import SwiftUI struct CharacterDetailGalleryView: View { + @StateObject var viewModel = CharacterDetailGalleryViewModel() + + private let columns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 3) + + // 갤러리 데이터 + private let ownedCount: Int = 104 + private let totalCount: Int = 259 + + // 계산된 속성들 + private var ownershipPercentage: Int { + guard totalCount > 0 else { return 0 } + return Int(round(Double(ownedCount) / Double(totalCount) * 100)) + } + + private var progressBarWidth: CGFloat { + let maxWidth: CGFloat = 352 // 전체 진행률 바의 최대 너비 + guard totalCount > 0 else { return 0 } + let percentage = Double(ownedCount) / Double(totalCount) + return maxWidth * percentage + } + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + BaseView(isLoading: $viewModel.isLoading) { + VStack(spacing: 0) { + // 상단 여백 24px + Spacer() + .frame(height: 24) + + // 보유 정보 섹션 + collectionInfoView() + .padding(.horizontal, 24) + .padding(.bottom, 8) + + // 갤러리 그리드 + LazyVGrid(columns: columns, spacing: 2) { + ForEach(0..<4, id: \.self) { index in + galleryImageView(index: index) + } + } + .padding(.horizontal, 0) + + Spacer() + } + .background(Color(hex: "#131313")) + } + } + + @ViewBuilder + private func collectionInfoView() -> some View { + VStack(spacing: 8) { + // 상단 정보 (계산된 % 보유중, 정보 아이콘, 개수) + HStack { + Text("\(ownershipPercentage)% 보유중") + .font(.custom(Font.preBold.rawValue, size: 18)) + .foregroundColor(.white) + + Spacer() + + HStack(spacing: 4) { + Text("\(ownedCount)") + .font(.custom(Font.preRegular.rawValue, size: 16)) + .foregroundColor(Color(hex: "#FDD453")) + + Text("/") + .font(.custom(Font.preRegular.rawValue, size: 16)) + .foregroundColor(.white) + + Text("\(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(index: Int) -> some View { + ZStack { + // 이미지 + AsyncImage(url: URL(string: "https://picsum.photos/400/500")) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .fill(Color.gray.opacity(0.3)) + } + .frame(width: 132, height: 165) + .clipped() + .cornerRadius(0) + + // 자물쇠가 있는 이미지들 (index 2, 3) + if index >= 2 { + // 어두운 오버레이 + 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("20") + .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) + } + } + } + } } } diff --git a/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryViewModel.swift b/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryViewModel.swift new file mode 100644 index 0000000..c234f8e --- /dev/null +++ b/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryViewModel.swift @@ -0,0 +1,23 @@ +// +// CharacterDetailGalleryViewModel.swift +// SodaLive +// +// Created by klaus on 9/2/25. +// + +import Foundation +import Combine +import Moya + +final class CharacterDetailGalleryViewModel: ObservableObject { + // MARK: - Published State + @Published var isLoading: Bool = false + @Published var errorMessage: String = "" + @Published var isShowPopup = false + + // MARK: - Private + private let repository = CharacterDetailGalleryRepository() + private var subscription = Set() + + // MARK: - Public Methods +} diff --git a/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterImageListResponse.swift b/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterImageListResponse.swift new file mode 100644 index 0000000..3e3b0b0 --- /dev/null +++ b/SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterImageListResponse.swift @@ -0,0 +1,20 @@ +// +// CharacterImageListResponse.swift +// SodaLive +// +// Created by klaus on 9/2/25. +// + +struct CharacterImageListResponse: Decodable { + let totalCount: Int + let ownedCount: Int + let items: [CharacterImageListItemResponse] +} + +struct CharacterImageListItemResponse: Decodable { + let id: Int + let imageUrl: String + let isOwned: Bool + let imagePriceCan: Int + +}