feat(character-detail/gallery): 갤러리 추가 및 이미지 목록 연동

- 에셋 추가: ic_new_lock
- 그리드 UI 적용
This commit is contained in:
Yu Sung
2025-09-02 02:37:35 +09:00
parent f11120b8d0
commit 392184fd34
6 changed files with 229 additions and 1 deletions

View File

@@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

View File

@@ -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<CharacterApi>()
}

View File

@@ -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)
}
}
}
}
}
}

View File

@@ -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<AnyCancellable>()
// MARK: - Public Methods
}

View File

@@ -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
}