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
|
import SwiftUI
|
||||||
|
|
||||||
struct CharacterDetailGalleryView: View {
|
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 {
|
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