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