feat(chat): 채팅 탭 추가 및 캐릭터/톡 내부 탭 구현
- ChatTabView 신설: 앱 바 + 내부 탭(캐릭터/톡) 전환 구성 - 커스텀 탭 적용 - indicatorHeight: 4, indicatorColor: #3bb9f1 - tabText color: #b0bec5 - 폰트: 선택 전 Pretendard-Regular, 선택 후 Pretendard-Bold - HomeView/BottomTabView에 ChatTabView 연동 - CharacterView/TalkView 플레이스홀더 추가
This commit is contained in:
		
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -7,6 +7,7 @@
 | 
			
		||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
 | 
			
		||||
 | 
			
		||||
# User-specific stuff
 | 
			
		||||
.idea/
 | 
			
		||||
.idea/**/workspace.xml
 | 
			
		||||
.idea/**/tasks.xml
 | 
			
		||||
.idea/**/usage.statistics.xml
 | 
			
		||||
@@ -276,4 +277,7 @@ xcuserdata
 | 
			
		||||
/*.gcno
 | 
			
		||||
**/xcshareddata/WorkspaceSettings.xcsettings
 | 
			
		||||
 | 
			
		||||
.kiro/
 | 
			
		||||
.junie/
 | 
			
		||||
 | 
			
		||||
# End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swift,swiftpackagemanager,swiftpm,fastlane,cocoapods
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								SodaLive/Sources/Chat/Character/CharacterView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								SodaLive/Sources/Chat/Character/CharacterView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
//
 | 
			
		||||
//  CharacterView.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 8/29/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct CharacterView: View {
 | 
			
		||||
  var body: some View {
 | 
			
		||||
    VStack(spacing: 12) {
 | 
			
		||||
      Spacer()
 | 
			
		||||
      Text("캐릭터 페이지 (준비중)")
 | 
			
		||||
        .font(.custom(Font.preMedium.rawValue, size: 16))
 | 
			
		||||
        .multilineTextAlignment(.center)
 | 
			
		||||
      Spacer()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
  CharacterView()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										119
									
								
								SodaLive/Sources/Chat/ChatTabView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								SodaLive/Sources/Chat/ChatTabView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
//
 | 
			
		||||
//  ChatTabView.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 8/29/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct ChatTabView: View {
 | 
			
		||||
    @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
 | 
			
		||||
    
 | 
			
		||||
    private enum InnerTab: Int, CaseIterable {
 | 
			
		||||
        case character = 0
 | 
			
		||||
        case talk = 1
 | 
			
		||||
        
 | 
			
		||||
        var title: String {
 | 
			
		||||
            switch self {
 | 
			
		||||
            case .character: return "캐릭터"
 | 
			
		||||
            case .talk: return "톡"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @State private var selectedTab: InnerTab = .character
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack(alignment: .leading, spacing: 0) {
 | 
			
		||||
            // 앱 바
 | 
			
		||||
            HStack(spacing: 24) {
 | 
			
		||||
                Image("img_text_logo")
 | 
			
		||||
                
 | 
			
		||||
                Spacer()
 | 
			
		||||
                
 | 
			
		||||
                if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
 | 
			
		||||
                    Image("ic_search_white")
 | 
			
		||||
                        .onTapGesture {
 | 
			
		||||
                            AppState
 | 
			
		||||
                                .shared
 | 
			
		||||
                                .setAppStep(step: .search)
 | 
			
		||||
                        }
 | 
			
		||||
                    
 | 
			
		||||
                    Image("ic_can_circle")
 | 
			
		||||
                        .onTapGesture {
 | 
			
		||||
                            AppState
 | 
			
		||||
                                .shared
 | 
			
		||||
                                .setAppStep(step: .canCharge(refresh: {}))
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .padding(.horizontal, 24)
 | 
			
		||||
            .padding(.vertical, 20)
 | 
			
		||||
            
 | 
			
		||||
            // 내부 탭 (캐릭터 / 톡)
 | 
			
		||||
            HStack(spacing: 0) {
 | 
			
		||||
                ChatInnerTab(
 | 
			
		||||
                    title: InnerTab.character.title,
 | 
			
		||||
                    isSelected: selectedTab == .character,
 | 
			
		||||
                    onTap: { if selectedTab != .character { selectedTab = .character } }
 | 
			
		||||
                )
 | 
			
		||||
                
 | 
			
		||||
                ChatInnerTab(
 | 
			
		||||
                    title: InnerTab.talk.title,
 | 
			
		||||
                    isSelected: selectedTab == .talk,
 | 
			
		||||
                    onTap: { if selectedTab != .talk { selectedTab = .talk } }
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            .padding(.bottom, 12)
 | 
			
		||||
            
 | 
			
		||||
            Group {
 | 
			
		||||
                switch selectedTab {
 | 
			
		||||
                case .character:
 | 
			
		||||
                    CharacterView()
 | 
			
		||||
                case .talk:
 | 
			
		||||
                    TalkView()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ChatInnerTab: View {
 | 
			
		||||
    let title: String
 | 
			
		||||
    let isSelected: Bool
 | 
			
		||||
    let onTap: () -> Void
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Button(action: onTap) {
 | 
			
		||||
            VStack(spacing: 0) {
 | 
			
		||||
                Spacer()
 | 
			
		||||
                
 | 
			
		||||
                Text(title)
 | 
			
		||||
                    .font(
 | 
			
		||||
                        .custom(
 | 
			
		||||
                            isSelected ? Font.preBold.rawValue : Font.preRegular.rawValue,
 | 
			
		||||
                            size: 18
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    .foregroundColor(Color(hex: "b0bec5"))
 | 
			
		||||
                    .frame(maxWidth: .infinity)
 | 
			
		||||
                
 | 
			
		||||
                Spacer()
 | 
			
		||||
                
 | 
			
		||||
                Rectangle()
 | 
			
		||||
                    .frame(maxWidth: .infinity)
 | 
			
		||||
                    .frame(height: 4)
 | 
			
		||||
                    .foregroundColor(Color(hex: "3bb9f1").opacity(isSelected ? 1 : 0))
 | 
			
		||||
            }
 | 
			
		||||
            .frame(height: 50)
 | 
			
		||||
            .contentShape(Rectangle())
 | 
			
		||||
        }
 | 
			
		||||
        .buttonStyle(PlainButtonStyle())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
    ChatTabView()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								SodaLive/Sources/Chat/Talk/TalkView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								SodaLive/Sources/Chat/Talk/TalkView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
//
 | 
			
		||||
//  TalkView.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 8/29/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct TalkView: View {
 | 
			
		||||
  var body: some View {
 | 
			
		||||
    VStack(spacing: 12) {
 | 
			
		||||
      Spacer()
 | 
			
		||||
      Text("톡 페이지 (준비중)")
 | 
			
		||||
        .font(.custom(Font.preMedium.rawValue, size: 16))
 | 
			
		||||
        .multilineTextAlignment(.center)
 | 
			
		||||
      Spacer()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
  TalkView()
 | 
			
		||||
}
 | 
			
		||||
@@ -13,7 +13,7 @@ struct BottomTabView: View {
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        HStack(spacing: 0) {
 | 
			
		||||
            let tabWidth = width / 3
 | 
			
		||||
            let tabWidth = width / 4
 | 
			
		||||
            
 | 
			
		||||
            TabButton(
 | 
			
		||||
                title: "홈",
 | 
			
		||||
@@ -40,6 +40,31 @@ struct BottomTabView: View {
 | 
			
		||||
                width: tabWidth
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            TabButton(
 | 
			
		||||
                title: "채팅",
 | 
			
		||||
                action: {
 | 
			
		||||
                    if currentTab != .chat {
 | 
			
		||||
                        currentTab = .chat
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                image: {
 | 
			
		||||
                    currentTab == .chat ?
 | 
			
		||||
                    "ic_chat_selected" :
 | 
			
		||||
                    "ic_chat"
 | 
			
		||||
                },
 | 
			
		||||
                fontName: {
 | 
			
		||||
                    currentTab == .chat ?
 | 
			
		||||
                    Font.bold.rawValue :
 | 
			
		||||
                    Font.medium.rawValue
 | 
			
		||||
                },
 | 
			
		||||
                color: {
 | 
			
		||||
                    currentTab == .chat ?
 | 
			
		||||
                    Color.button :
 | 
			
		||||
                    Color.graybb
 | 
			
		||||
                },
 | 
			
		||||
                width: tabWidth
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
            TabButton(
 | 
			
		||||
                title: "라이브",
 | 
			
		||||
                action: {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ struct HomeView: View {
 | 
			
		||||
    
 | 
			
		||||
    private let liveView = LiveView()
 | 
			
		||||
    private let homeTabView = HomeTabView()
 | 
			
		||||
    private let chatTabView = ChatTabView()
 | 
			
		||||
    
 | 
			
		||||
    @State private var isShowPlayer = false
 | 
			
		||||
    @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
 | 
			
		||||
@@ -32,6 +33,10 @@ struct HomeView: View {
 | 
			
		||||
                            .frame(width: viewModel.currentTab == .home ? proxy.size.width : 0)
 | 
			
		||||
                            .opacity(viewModel.currentTab == .home ? 1.0 : 0.01)
 | 
			
		||||
                        
 | 
			
		||||
                        chatTabView
 | 
			
		||||
                            .frame(width: viewModel.currentTab == .chat ? proxy.size.width : 0)
 | 
			
		||||
                            .opacity(viewModel.currentTab == .chat ? 1.0 : 0.01)
 | 
			
		||||
                        
 | 
			
		||||
                        liveView
 | 
			
		||||
                            .frame(width: viewModel.currentTab == .live ? proxy.size.width : 0)
 | 
			
		||||
                            .opacity(viewModel.currentTab == .live ? 1.0 : 0.01)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ final class HomeViewModel: ObservableObject {
 | 
			
		||||
    private let playbackTrackingRepository = PlaybackTrackingRepository()
 | 
			
		||||
    
 | 
			
		||||
    enum CurrentTab: String {
 | 
			
		||||
        case home, live, mypage
 | 
			
		||||
        case home, chat, live, mypage
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @Published var currentTab: CurrentTab = AppState.shared.startTab
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user