From 392184fd344d81c2b47e13078f375902ab1fbd62 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 2 Sep 2025 02:37:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(character-detail/gallery):=20=EA=B0=A4?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 에셋 추가: ic_new_lock - 그리드 UI 적용 --- .../ic_new_lock.imageset/Contents.json | 21 +++ .../ic_new_lock.imageset/ic_new_lock.png | Bin 0 -> 359 bytes .../CharacterDetailGalleryRepository.swift | 15 ++ .../Gallery/CharacterDetailGalleryView.swift | 151 +++++++++++++++++- .../CharacterDetailGalleryViewModel.swift | 23 +++ .../Gallery/CharacterImageListResponse.swift | 20 +++ 6 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/Contents.json create mode 100644 SodaLive/Resources/Assets.xcassets/ic_new_lock.imageset/ic_new_lock.png create mode 100644 SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryRepository.swift create mode 100644 SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterDetailGalleryViewModel.swift create mode 100644 SodaLive/Sources/Chat/Character/Detail/Gallery/CharacterImageListResponse.swift 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 0000000000000000000000000000000000000000..a6a13de46cddc9a648f412854db3d6320003423b GIT binary patch literal 359 zcmeAS@N?(olHy`uVBq!ia0vp^5ylrKeYp5)mPDW3=ed6A&%FX(l~~0tui`XuYuNhZ zhu3b6<$9BN7fG9_bH}F7aAK$pUtu3_a7kTg=5ZAPWnTklF2?@{j%c5>e{ui86U)O) z|EzY{Ry6q>pZxXjmFX(WWX(C2FOtjoZ~d>&yx8{I)X1|D+`w>P@O1TaS?83{1OT=v Bi~|4w literal 0 HcmV?d00001 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 + +}