feat(character-detail/gallery): 갤러리 추가 및 이미지 목록 연동
- 에셋 추가: ic_new_lock - 그리드 UI 적용
This commit is contained in:
21
SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/ic_new_lock.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/ic_new_lock.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 359 B |
@@ -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>()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user