feat(character): 인기 캐릭터 섹션 추가
This commit is contained in:
@@ -12,9 +12,16 @@ struct CharacterItemView: View {
|
||||
|
||||
let character: Character
|
||||
let size: CGFloat
|
||||
let rank: Int
|
||||
let isShowRank: Bool
|
||||
|
||||
private var capHeight: CGFloat {
|
||||
UIFont(name: Font.preBold.rawValue, size: 72)?.capHeight ?? 72
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
KFImage(URL(string: character.imageUrl))
|
||||
.placeholder { Color.gray.opacity(0.2) }
|
||||
.retry(maxCount: 2, interval: .seconds(1))
|
||||
@@ -25,6 +32,15 @@ struct CharacterItemView: View {
|
||||
.clipped()
|
||||
.cornerRadius(12)
|
||||
|
||||
if isShowRank {
|
||||
Text("\(rank)")
|
||||
.font(.custom(Font.preBold.rawValue, size: 72))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.frame(height: capHeight)
|
||||
}
|
||||
}
|
||||
|
||||
Text(character.name)
|
||||
.font(.custom(Font.preRegular.rawValue, size: 18))
|
||||
.foregroundColor(.white)
|
||||
@@ -45,6 +61,8 @@ struct CharacterItemView: View {
|
||||
#Preview {
|
||||
CharacterItemView(
|
||||
character: Character(characterId: 1, name: "찰리", description: "새로운 친구", imageUrl: "https://picsum.photos/300"),
|
||||
size: 168
|
||||
size: 168,
|
||||
rank: 20,
|
||||
isShowRank: true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct CharacterSectionView: View {
|
||||
let title: String
|
||||
let items: [Character]
|
||||
let isShowRank: Bool
|
||||
var onTap: (Character) -> Void = { _ in }
|
||||
|
||||
var body: some View {
|
||||
@@ -25,7 +26,9 @@ struct CharacterSectionView: View {
|
||||
let item = items[idx]
|
||||
CharacterItemView(
|
||||
character: item,
|
||||
size: screenSize().width * 0.42
|
||||
size: screenSize().width * 0.42,
|
||||
rank: idx + 1,
|
||||
isShowRank: isShowRank
|
||||
)
|
||||
.onTapGesture { onTap(item) }
|
||||
}
|
||||
@@ -42,7 +45,8 @@ struct CharacterSectionView: View {
|
||||
items: [
|
||||
Character(characterId: 1, name: "찰리", description: "새로운 친구", imageUrl: "https://picsum.photos/300"),
|
||||
Character(characterId: 2, name: "데이지", description: "", imageUrl: "https://picsum.photos/300")
|
||||
]
|
||||
],
|
||||
isShowRank: true
|
||||
)
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
|
||||
@@ -35,11 +35,23 @@ struct CharacterView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 인기 캐릭터 섹션
|
||||
if !viewModel.popularCharacters.isEmpty {
|
||||
CharacterSectionView(
|
||||
title: "인기 캐릭터",
|
||||
items: viewModel.popularCharacters,
|
||||
isShowRank: true
|
||||
) { ch in
|
||||
onSelectCharacter(ch.characterId)
|
||||
}
|
||||
}
|
||||
|
||||
// 신규 캐릭터 섹션
|
||||
if !viewModel.newCharacters.isEmpty {
|
||||
CharacterSectionView(
|
||||
title: "신규 캐릭터",
|
||||
items: viewModel.newCharacters
|
||||
items: viewModel.newCharacters,
|
||||
isShowRank: false
|
||||
) { ch in
|
||||
onSelectCharacter(ch.characterId)
|
||||
}
|
||||
@@ -52,7 +64,8 @@ struct CharacterView: View {
|
||||
let section = viewModel.curations[idx]
|
||||
CharacterSectionView(
|
||||
title: section.title,
|
||||
items: section.characters
|
||||
items: section.characters,
|
||||
isShowRank: false
|
||||
) { ch in
|
||||
onSelectCharacter(ch.characterId)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ final class CharacterViewModel: ObservableObject {
|
||||
// MARK: - Published State
|
||||
@Published private(set) var banners: [CharacterBannerResponse] = []
|
||||
@Published private(set) var recentCharacters: [RecentCharacter] = []
|
||||
@Published private(set) var popularCharacters: [Character] = []
|
||||
@Published private(set) var newCharacters: [Character] = []
|
||||
@Published private(set) var curations: [CurationSection] = []
|
||||
|
||||
@@ -47,6 +48,7 @@ final class CharacterViewModel: ObservableObject {
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.banners = data.banners
|
||||
self.recentCharacters = data.recentCharacters
|
||||
self.popularCharacters = data.popularCharacters
|
||||
self.newCharacters = data.newCharacters
|
||||
self.curations = data.curationSections.filter { !$0.characters.isEmpty }
|
||||
} else {
|
||||
|
||||
@@ -87,7 +87,9 @@ struct CharacterDetailView: View {
|
||||
description: otherCharacter.tags,
|
||||
imageUrl: otherCharacter.imageUrl
|
||||
),
|
||||
size: screenSize().width * 0.42
|
||||
size: screenSize().width * 0.42,
|
||||
rank: 0,
|
||||
isShowRank: false
|
||||
)
|
||||
.onTapGesture {
|
||||
// 캐릭터 변경 후 스크롤을 최상단으로 이동
|
||||
|
||||
@@ -105,34 +105,33 @@ final class ChatRoomViewModel: ObservableObject {
|
||||
|
||||
showSendingMessage = true
|
||||
repository.sendMessage(roomId: roomId, message: message)
|
||||
.sink { result in
|
||||
switch result {
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
self.showSendingMessage = false // 실패 시 복구
|
||||
self.errorMessage = error.localizedDescription
|
||||
self.isShowPopup = true
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<SendChatMessageResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.messages.append(contentsOf: data.messages)
|
||||
self.updateQuota(nextRechargeAtEpoch: data.nextRechargeAtEpoch)
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.errorMessage = decoded.message ??
|
||||
"다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.showSendingMessage = false
|
||||
self.showSendingMessage = false // 성공 시 종료
|
||||
} catch {
|
||||
self.showSendingMessage = false
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
|
||||
Reference in New Issue
Block a user