feat: 메인 홈
- 라이브, 인기 크리, 최신 콘텐츠, 이벤트 배너 UI 추가
This commit is contained in:
		
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/img_live.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/img_live.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
{
 | 
			
		||||
  "images" : [
 | 
			
		||||
    {
 | 
			
		||||
      "filename" : "img_live.png",
 | 
			
		||||
      "idiom" : "universal",
 | 
			
		||||
      "scale" : "1x"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "universal",
 | 
			
		||||
      "scale" : "2x"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "universal",
 | 
			
		||||
      "scale" : "3x"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "info" : {
 | 
			
		||||
    "author" : "xcode",
 | 
			
		||||
    "version" : 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/img_live.imageset/img_live.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/img_live.imageset/img_live.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										12
									
								
								SodaLive/Resources/Assets.xcassets/rank_1.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								SodaLive/Resources/Assets.xcassets/rank_1.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "images" : [
 | 
			
		||||
    {
 | 
			
		||||
      "filename" : "rank_1.pdf",
 | 
			
		||||
      "idiom" : "universal"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "info" : {
 | 
			
		||||
    "author" : "xcode",
 | 
			
		||||
    "version" : 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/rank_1.imageset/rank_1.pdf
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/rank_1.imageset/rank_1.pdf
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										12
									
								
								SodaLive/Resources/Assets.xcassets/rank_2.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								SodaLive/Resources/Assets.xcassets/rank_2.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "images" : [
 | 
			
		||||
    {
 | 
			
		||||
      "filename" : "rank_2.pdf",
 | 
			
		||||
      "idiom" : "universal"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "info" : {
 | 
			
		||||
    "author" : "xcode",
 | 
			
		||||
    "version" : 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/rank_2.imageset/rank_2.pdf
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/rank_2.imageset/rank_2.pdf
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										12
									
								
								SodaLive/Resources/Assets.xcassets/rank_3.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								SodaLive/Resources/Assets.xcassets/rank_3.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "images" : [
 | 
			
		||||
    {
 | 
			
		||||
      "filename" : "rank_3.pdf",
 | 
			
		||||
      "idiom" : "universal"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "info" : {
 | 
			
		||||
    "author" : "xcode",
 | 
			
		||||
    "version" : 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/rank_3.imageset/rank_3.pdf
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/rank_3.imageset/rank_3.pdf
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -242,6 +242,10 @@
 | 
			
		||||
		<string>gmarket_sans_bold.otf</string>
 | 
			
		||||
		<string>gmarket_sans_medium.otf</string>
 | 
			
		||||
		<string>gmarket_sans_light.otf</string>
 | 
			
		||||
        <string>Pretendard-Bold.otf</string>
 | 
			
		||||
        <string>Pretendard-Medium.otf</string>
 | 
			
		||||
        <string>Pretendard-Light.otf</string>
 | 
			
		||||
        <string>Pretendard-Regular.otf</string>
 | 
			
		||||
	</array>
 | 
			
		||||
	<key>UIBackgroundModes</key>
 | 
			
		||||
	<array>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Font/Pretendard-Bold.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Font/Pretendard-Bold.otf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Font/Pretendard-Light.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Font/Pretendard-Light.otf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Font/Pretendard-Medium.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Font/Pretendard-Medium.otf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Font/Pretendard-Regular.otf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Font/Pretendard-Regular.otf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -242,6 +242,10 @@
 | 
			
		||||
		<string>gmarket_sans_bold.otf</string>
 | 
			
		||||
		<string>gmarket_sans_medium.otf</string>
 | 
			
		||||
		<string>gmarket_sans_light.otf</string>
 | 
			
		||||
        <string>Pretendard-Bold.otf</string>
 | 
			
		||||
        <string>Pretendard-Medium.otf</string>
 | 
			
		||||
        <string>Pretendard-Light.otf</string>
 | 
			
		||||
        <string>Pretendard-Regular.otf</string>
 | 
			
		||||
	</array>
 | 
			
		||||
	<key>UIBackgroundModes</key>
 | 
			
		||||
	<array>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,6 @@
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>aps-environment</key>
 | 
			
		||||
	<string>development</string>
 | 
			
		||||
	<key>com.apple.developer.applesignin</key>
 | 
			
		||||
	<array>
 | 
			
		||||
		<string>Default</string>
 | 
			
		||||
	</array>
 | 
			
		||||
	<key>com.apple.developer.associated-domains</key>
 | 
			
		||||
	<array>
 | 
			
		||||
		<string>applinks:voiceon.onelink.me</string>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,23 +23,6 @@ struct ContentItemView: View {
 | 
			
		||||
                    .cornerRadius(16)
 | 
			
		||||
                
 | 
			
		||||
                HStack(alignment: .top, spacing: 0) {
 | 
			
		||||
                    Text("신작")
 | 
			
		||||
                        .font(.custom(Font.medium.rawValue, size: 12))
 | 
			
		||||
                        .foregroundColor(.white)
 | 
			
		||||
                        .padding(.horizontal, 10)
 | 
			
		||||
                        .padding(.vertical, 3)
 | 
			
		||||
                        .background(
 | 
			
		||||
                            LinearGradient(
 | 
			
		||||
                                gradient: Gradient(stops: [
 | 
			
		||||
                                    .init(color: Color(hex: "0001B1"), location: 0.24),
 | 
			
		||||
                                    .init(color: Color(hex: "3B5FF1"), location: 1.0)
 | 
			
		||||
                                ]),
 | 
			
		||||
                                startPoint: .top,
 | 
			
		||||
                                endPoint: .bottom
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
                        .cornerRadius(16, corners: [.topLeft, .bottomRight])
 | 
			
		||||
                    
 | 
			
		||||
                    Spacer()
 | 
			
		||||
                    
 | 
			
		||||
                    if item.isPointAvailable {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,24 +16,23 @@ struct ContentMainContentThemeView: View {
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
            HStack(alignment: .top, spacing: 8) {
 | 
			
		||||
            HStack(alignment: .top, spacing: 16) {
 | 
			
		||||
                ForEach(0..<themeList.count, id: \.self) { index in
 | 
			
		||||
                    let theme = themeList[index]
 | 
			
		||||
                    Text(theme)
 | 
			
		||||
                        .font(.custom(Font.medium.rawValue, size: 14.7))
 | 
			
		||||
                        .foregroundColor(selectedTheme == theme ? Color.button : Color.gray77)
 | 
			
		||||
                        .padding(.horizontal, 13.3)
 | 
			
		||||
                        .padding(.vertical, 9.3)
 | 
			
		||||
                        .border(
 | 
			
		||||
                            selectedTheme == theme ? Color.button : Color.grayee,
 | 
			
		||||
                            width: 1
 | 
			
		||||
                        .font(
 | 
			
		||||
                            .custom(
 | 
			
		||||
                                selectedTheme == theme ? Font.preBold.rawValue : Font.preRegular.rawValue,
 | 
			
		||||
                                size: 16
 | 
			
		||||
                            )
 | 
			
		||||
                        .cornerRadius(16.7)
 | 
			
		||||
                        .overlay(
 | 
			
		||||
                            RoundedRectangle(cornerRadius: CGFloat(16.7))
 | 
			
		||||
                                .stroke(lineWidth: 1)
 | 
			
		||||
                                .foregroundColor(selectedTheme == theme ? Color.button : Color.grayee)
 | 
			
		||||
                        )
 | 
			
		||||
                        .foregroundColor(.white)
 | 
			
		||||
                        .padding(.horizontal, 24)
 | 
			
		||||
                        .padding(.vertical, 12)
 | 
			
		||||
                        .background(
 | 
			
		||||
                            selectedTheme == theme ? Color.button : Color(hex: "263238")
 | 
			
		||||
                        )
 | 
			
		||||
                        .cornerRadius(999)
 | 
			
		||||
                        .onTapGesture {
 | 
			
		||||
                            if selectedTheme != theme {
 | 
			
		||||
                                selectedTheme = theme
 | 
			
		||||
@@ -42,7 +41,7 @@ struct ContentMainContentThemeView: View {
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .padding(.horizontal, 13.3)
 | 
			
		||||
            .padding(.horizontal, 24)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -265,7 +265,6 @@ struct ContentMainTabHomeView: View {
 | 
			
		||||
                        .font(.custom(Font.medium.rawValue, size: 11))
 | 
			
		||||
                        .foregroundColor(Color.gray77)
 | 
			
		||||
                        .padding(.top, 30)
 | 
			
		||||
                        .padding(.horizontal, 13.3)
 | 
			
		||||
                    }
 | 
			
		||||
                    .onAppear {
 | 
			
		||||
                        viewModel.fetchData()
 | 
			
		||||
 
 | 
			
		||||
@@ -150,25 +150,29 @@ struct ContentMainTabHomeRankCreatorView: View {
 | 
			
		||||
                    id: 1,
 | 
			
		||||
                    nickname: "User1",
 | 
			
		||||
                    tags: "",
 | 
			
		||||
                    profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
 | 
			
		||||
                    profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
 | 
			
		||||
                    followerCount: 1000
 | 
			
		||||
                ),
 | 
			
		||||
                GetExplorerSectionCreatorResponse(
 | 
			
		||||
                    id: 2,
 | 
			
		||||
                    nickname: "User2",
 | 
			
		||||
                    tags: "",
 | 
			
		||||
                    profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
 | 
			
		||||
                    profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
 | 
			
		||||
                    followerCount: 1000
 | 
			
		||||
                ),
 | 
			
		||||
                GetExplorerSectionCreatorResponse(
 | 
			
		||||
                    id: 3,
 | 
			
		||||
                    nickname: "User3",
 | 
			
		||||
                    tags: "",
 | 
			
		||||
                    profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
 | 
			
		||||
                    profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
 | 
			
		||||
                    followerCount: 1000
 | 
			
		||||
                ),
 | 
			
		||||
                GetExplorerSectionCreatorResponse(
 | 
			
		||||
                    id: 4,
 | 
			
		||||
                    nickname: "User4",
 | 
			
		||||
                    tags: "",
 | 
			
		||||
                    profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
 | 
			
		||||
                    profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
 | 
			
		||||
                    followerCount: 1000
 | 
			
		||||
                )
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								SodaLive/Sources/Content/Series/SeriesItemView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								SodaLive/Sources/Content/Series/SeriesItemView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
//
 | 
			
		||||
//  SeriesItemView.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 7/11/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct SeriesItemView: View {
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
    SeriesItemView()
 | 
			
		||||
}
 | 
			
		||||
@@ -24,5 +24,6 @@ struct GetExplorerSectionCreatorResponse: Decodable, Hashable {
 | 
			
		||||
    let nickname: String
 | 
			
		||||
    let tags: String
 | 
			
		||||
    let profileImageUrl: String
 | 
			
		||||
    let followerCount: Int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,4 +11,12 @@ enum Font: String {
 | 
			
		||||
    case medium = "GmarketSansMedium"
 | 
			
		||||
    
 | 
			
		||||
    case light = "GmarketSansLight"
 | 
			
		||||
    
 | 
			
		||||
    case preBold = "Pretendard-Bold"
 | 
			
		||||
    
 | 
			
		||||
    case preMedium = "Pretendard-Medium"
 | 
			
		||||
    
 | 
			
		||||
    case preRegular = "Pretendard-Regular"
 | 
			
		||||
    
 | 
			
		||||
    case preLight = "Pretendard-Light"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										85
									
								
								SodaLive/Sources/Home/HomeCreatorRankingItemView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								SodaLive/Sources/Home/HomeCreatorRankingItemView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
//
 | 
			
		||||
//  HomeCreatorRankingItemView.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 7/11/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import Kingfisher
 | 
			
		||||
 | 
			
		||||
struct HomeCreatorRankingItemView: View {
 | 
			
		||||
    
 | 
			
		||||
    let rank: Int
 | 
			
		||||
    let item: GetExplorerSectionCreatorResponse
 | 
			
		||||
    
 | 
			
		||||
    let crowns = ["rank_1", "rank_2", "rank_3"]
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        ZStack(alignment: .topLeading) {
 | 
			
		||||
            VStack(spacing: 0) {
 | 
			
		||||
                KFImage(URL(string: item.profileImageUrl))
 | 
			
		||||
                    .cancelOnDisappear(true)
 | 
			
		||||
                    .resizable()
 | 
			
		||||
                    .frame(width: 56, height: 56)
 | 
			
		||||
                    .clipShape(Circle())
 | 
			
		||||
                
 | 
			
		||||
                Text(item.nickname)
 | 
			
		||||
                    .font(.custom(Font.preRegular.rawValue, size: 18))
 | 
			
		||||
                    .foregroundColor(.white)
 | 
			
		||||
                    .padding(.top, 8)
 | 
			
		||||
                
 | 
			
		||||
                Text("팔로워")
 | 
			
		||||
                    .font(.custom(Font.preBold.rawValue, size: 14))
 | 
			
		||||
                    .foregroundColor(Color(hex: "78909C"))
 | 
			
		||||
                    .padding(.top, 12)
 | 
			
		||||
                
 | 
			
		||||
                Text("\(item.followerCount)")
 | 
			
		||||
                    .font(.custom(Font.preRegular.rawValue, size: 14))
 | 
			
		||||
                    .foregroundColor(Color(hex: "78909C"))
 | 
			
		||||
                    .padding(.top, 4)
 | 
			
		||||
            }
 | 
			
		||||
            .frame(width: 133, height: 188)
 | 
			
		||||
            .background(
 | 
			
		||||
                LinearGradient(
 | 
			
		||||
                    gradient: Gradient(colors: [Color(hex: "5ACDE1"), Color(hex: "2A339D")]),
 | 
			
		||||
                    startPoint: .topLeading,
 | 
			
		||||
                    endPoint: .bottom
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            .cornerRadius(16)
 | 
			
		||||
            .overlay {
 | 
			
		||||
                RoundedRectangle(cornerRadius: 16)
 | 
			
		||||
                    .strokeBorder(
 | 
			
		||||
                        LinearGradient(
 | 
			
		||||
                            gradient: Gradient(colors: [Color(hex: "9AE2F6"), .white.opacity(0)]),
 | 
			
		||||
                            startPoint: .top,
 | 
			
		||||
                            endPoint: .bottom
 | 
			
		||||
                        ),
 | 
			
		||||
                        lineWidth: 1
 | 
			
		||||
                    )
 | 
			
		||||
            }
 | 
			
		||||
            .padding(.top, 20)
 | 
			
		||||
            
 | 
			
		||||
            if rank <= 3 {
 | 
			
		||||
                Image(crowns[rank - 1])
 | 
			
		||||
                    .resizable()
 | 
			
		||||
                    .frame(width: 40, height: 40)
 | 
			
		||||
                    .padding(.leading, 10)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
    HomeCreatorRankingItemView(
 | 
			
		||||
        rank: 1,
 | 
			
		||||
        item: GetExplorerSectionCreatorResponse(
 | 
			
		||||
            id: 1,
 | 
			
		||||
            nickname: "유빈ASMR",
 | 
			
		||||
            tags: "",
 | 
			
		||||
            profileImageUrl: "https://cf.sodalive.net/profile/34806/34806-profile-49db6b45-bb1e-4dc7-917e-1a614a853f5f-4232-1752158072656",
 | 
			
		||||
            followerCount: 1000
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										86
									
								
								SodaLive/Sources/Home/HomeLatestContentView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								SodaLive/Sources/Home/HomeLatestContentView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
//
 | 
			
		||||
//  HomeLatestContentView.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 7/11/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct HomeLatestContentView: View {
 | 
			
		||||
    
 | 
			
		||||
    let onClickMore: () -> Void
 | 
			
		||||
    let themeList: [String]
 | 
			
		||||
    let contentList: [AudioContentMainItem]
 | 
			
		||||
    
 | 
			
		||||
    let selectTheme: (String) -> Void
 | 
			
		||||
    @State private var selectedTheme = "전체"
 | 
			
		||||
    
 | 
			
		||||
    let rows = [
 | 
			
		||||
        GridItem(.flexible(), alignment: .leading),
 | 
			
		||||
        GridItem(.flexible(), alignment: .leading)
 | 
			
		||||
    ]
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        HStack(spacing: 0) {
 | 
			
		||||
            Text("최신")
 | 
			
		||||
                .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                .foregroundColor(.button)
 | 
			
		||||
            
 | 
			
		||||
            Text(" 콘텐츠")
 | 
			
		||||
                .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                .foregroundColor(.white)
 | 
			
		||||
            
 | 
			
		||||
            Spacer()
 | 
			
		||||
            
 | 
			
		||||
            Text("전체보기")
 | 
			
		||||
                .font(.custom(Font.preRegular.rawValue, size: 14))
 | 
			
		||||
                .foregroundColor(.init(hex: "78909C"))
 | 
			
		||||
                .onTapGesture { onClickMore() }
 | 
			
		||||
        }
 | 
			
		||||
        .padding(.horizontal, 24)
 | 
			
		||||
        
 | 
			
		||||
        if !themeList.isEmpty {
 | 
			
		||||
            ContentMainContentThemeView(
 | 
			
		||||
                themeList: themeList,
 | 
			
		||||
                selectTheme: selectTheme,
 | 
			
		||||
                selectedTheme: $selectedTheme
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
            LazyHGrid(rows: rows, spacing: 16) {
 | 
			
		||||
                ForEach(0..<contentList.count, id: \.self) { index in
 | 
			
		||||
                    ContentItemView(item: contentList[index])
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .padding(.horizontal, 24)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
    HomeLatestContentView(
 | 
			
		||||
        onClickMore: {},
 | 
			
		||||
        themeList: ["전체", "테스트1", "테스트2"],
 | 
			
		||||
        contentList: [
 | 
			
		||||
            AudioContentMainItem(
 | 
			
		||||
                contentId: 1,
 | 
			
		||||
                creatorId: 3,
 | 
			
		||||
                title: "ㅓ처랴햐햫햐햐",
 | 
			
		||||
                coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
 | 
			
		||||
                creatorNickname: "유저1",
 | 
			
		||||
                isPointAvailable: true
 | 
			
		||||
            ),
 | 
			
		||||
            AudioContentMainItem(
 | 
			
		||||
                contentId: 2,
 | 
			
		||||
                creatorId: 8,
 | 
			
		||||
                title: "ㅓ처랴햐햫햐햐",
 | 
			
		||||
                coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
 | 
			
		||||
                creatorNickname: "유저2",
 | 
			
		||||
                isPointAvailable: false
 | 
			
		||||
            )
 | 
			
		||||
        ],
 | 
			
		||||
        selectTheme: { _ in }
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								SodaLive/Sources/Home/HomeLiveItemView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								SodaLive/Sources/Home/HomeLiveItemView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
//
 | 
			
		||||
//  HomeLiveItemView.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 7/11/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import Kingfisher
 | 
			
		||||
 | 
			
		||||
struct HomeLiveItemView: View {
 | 
			
		||||
    
 | 
			
		||||
    let item: GetRoomListResponse
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        HStack(spacing: 16) {
 | 
			
		||||
            ZStack(alignment: .bottom) {
 | 
			
		||||
                ZStack {
 | 
			
		||||
                    KFImage(URL(string: item.creatorProfileImage))
 | 
			
		||||
                        .cancelOnDisappear(true)
 | 
			
		||||
                        .resizable()
 | 
			
		||||
                        .frame(width: 62, height: 62)
 | 
			
		||||
                        .clipShape(Circle())
 | 
			
		||||
                }
 | 
			
		||||
                .padding(7)
 | 
			
		||||
                .overlay {
 | 
			
		||||
                    Circle()
 | 
			
		||||
                        .strokeBorder(lineWidth: 3)
 | 
			
		||||
                        .foregroundColor(.button)
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                Image("img_live")
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            VStack(alignment: .leading, spacing: 4) {
 | 
			
		||||
                Text(item.title)
 | 
			
		||||
                    .font(.custom(Font.preRegular.rawValue, size: 18))
 | 
			
		||||
                    .foregroundColor(.white)
 | 
			
		||||
                
 | 
			
		||||
                Text(item.creatorNickname)
 | 
			
		||||
                    .font(.custom(Font.preRegular.rawValue, size: 16))
 | 
			
		||||
                    .foregroundColor(Color(hex: "B0BEC5"))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .padding(.horizontal, 14)
 | 
			
		||||
        .padding(.vertical, 10)
 | 
			
		||||
        .frame(width: 282, alignment: .leading)
 | 
			
		||||
        .background(
 | 
			
		||||
            RadialGradient(
 | 
			
		||||
                gradient: Gradient(colors: [Color(hex: "5ACDE1"), Color(hex: "2A339D")]),
 | 
			
		||||
                center: .center,
 | 
			
		||||
                startRadius: 0,
 | 
			
		||||
                endRadius: 200
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        .cornerRadius(999)
 | 
			
		||||
        .overlay(
 | 
			
		||||
            RoundedRectangle(cornerRadius: 999)
 | 
			
		||||
                .strokeBorder(
 | 
			
		||||
                    LinearGradient(
 | 
			
		||||
                        colors: [Color(hex: "9AE2F6"), Color(hex: "FFFFFF")],
 | 
			
		||||
                        startPoint: .topLeading,
 | 
			
		||||
                        endPoint: .bottomTrailing
 | 
			
		||||
                    ),
 | 
			
		||||
                    lineWidth: 1
 | 
			
		||||
                )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Preview {
 | 
			
		||||
    HomeLiveItemView(
 | 
			
		||||
        item: GetRoomListResponse(
 | 
			
		||||
            roomId: 1,
 | 
			
		||||
            title: "네네코 마사로",
 | 
			
		||||
            content: "테스트",
 | 
			
		||||
            beginDateTime: "2025-08-10 15:00:00",
 | 
			
		||||
            numberOfParticipate: 1,
 | 
			
		||||
            numberOfPeople: 10,
 | 
			
		||||
            coverImageUrl: "https://cf.sodalive.net/live_room_cover/18038/18038-cover-8c3cb985-733d-4425-8eaf-ef753064d371-2283-1751800412922",
 | 
			
		||||
            isAdult: false,
 | 
			
		||||
            price: 0,
 | 
			
		||||
            tags: [""],
 | 
			
		||||
            channelName: "",
 | 
			
		||||
            creatorProfileImage: "https://cf.sodalive.net/profile/34806/34806-profile-49db6b45-bb1e-4dc7-917e-1a614a853f5f-4232-1752158072656",
 | 
			
		||||
            creatorNickname: "설린",
 | 
			
		||||
            creatorId: 1,
 | 
			
		||||
            isReservation: false,
 | 
			
		||||
            isPrivateRoom: false
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -12,4 +12,33 @@ import Moya
 | 
			
		||||
 | 
			
		||||
class HomeTabRepository {
 | 
			
		||||
    private let api = MoyaProvider<HomeApi>()
 | 
			
		||||
    
 | 
			
		||||
    func fetchData() -> AnyPublisher<Response, MoyaError> {
 | 
			
		||||
        return api.requestPublisher(
 | 
			
		||||
            .getHomeData(
 | 
			
		||||
                isAdultContentVisible: UserDefaults.isAdultContentVisible(),
 | 
			
		||||
                contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func getLatestContentByTheme(theme: String) -> AnyPublisher<Response, MoyaError> {
 | 
			
		||||
        return api.requestPublisher(
 | 
			
		||||
            .getLatestContentByTheme(
 | 
			
		||||
                theme: theme,
 | 
			
		||||
                isAdultContentVisible: UserDefaults.isAdultContentVisible(),
 | 
			
		||||
                contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek) -> AnyPublisher<Response, MoyaError> {
 | 
			
		||||
        return api.requestPublisher(
 | 
			
		||||
            .getDayOfWeekSeriesList(
 | 
			
		||||
                dayOfWeek: dayOfWeek,
 | 
			
		||||
                isAdultContentVisible: UserDefaults.isAdultContentVisible(),
 | 
			
		||||
                contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
struct HomeTabView: View {
 | 
			
		||||
    @StateObject var viewModel = ContentMainTabHomeViewModel()
 | 
			
		||||
    @StateObject var viewModel = HomeTabViewModel()
 | 
			
		||||
    @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
 | 
			
		||||
    @AppStorage("role") private var role: String = UserDefaults.string(forKey: UserDefaultsKey.role)
 | 
			
		||||
    
 | 
			
		||||
@@ -35,80 +35,88 @@ struct HomeTabView: View {
 | 
			
		||||
                    
 | 
			
		||||
                    ScrollView(.vertical, showsIndicators: false) {
 | 
			
		||||
                        VStack(alignment: .leading, spacing: 48) {
 | 
			
		||||
                            if !viewModel.liveList.isEmpty {
 | 
			
		||||
                                VStack(alignment: .leading, spacing: 16) {
 | 
			
		||||
                                    HStack(spacing: 0) {
 | 
			
		||||
                                        Text("지금")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                            .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                            .foregroundColor(.button)
 | 
			
		||||
                                        
 | 
			
		||||
                                        Text(" 라이브중")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                            .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                            .foregroundColor(.white)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.horizontal, 24)
 | 
			
		||||
                                    
 | 
			
		||||
                                    ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                        HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                            ForEach(0..<viewModel.liveList.count, id: \.self) {
 | 
			
		||||
                                                HomeLiveItemView(item: viewModel.liveList[$0])
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                        .padding(.horizontal, 24)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                            if !viewModel.creatorRanking.isEmpty {
 | 
			
		||||
                                VStack(alignment: .leading, spacing: 16) {
 | 
			
		||||
                                    HStack(spacing: 0) {
 | 
			
		||||
                                        Text("인기")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                            .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                            .foregroundColor(.button)
 | 
			
		||||
                                        
 | 
			
		||||
                                        Text(" 크리에이터")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                            .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                            .foregroundColor(.white)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.horizontal, 24)
 | 
			
		||||
                                    
 | 
			
		||||
                                    ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                        HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                            ForEach(0..<viewModel.creatorRanking.count, id: \.self) {
 | 
			
		||||
                                                HomeCreatorRankingItemView(rank: $0 + 1, item: viewModel.creatorRanking[$0])
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                        .padding(.horizontal, 24)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                            VStack(alignment: .leading, spacing: 16) {
 | 
			
		||||
                                HStack(spacing: 0) {
 | 
			
		||||
                                    Text("최신")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.button)
 | 
			
		||||
                                    
 | 
			
		||||
                                    Text(" 콘텐츠")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.white)
 | 
			
		||||
                                }
 | 
			
		||||
                                
 | 
			
		||||
                                ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                    HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                HomeLatestContentView(
 | 
			
		||||
                                    onClickMore: {
 | 
			
		||||
                                        AppState.shared
 | 
			
		||||
                                            .setAppStep(step: .newContentAll(isFree: false))
 | 
			
		||||
                                    },
 | 
			
		||||
                                    themeList: viewModel.latestContentThemeList,
 | 
			
		||||
                                    contentList: viewModel.latestContentList
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    viewModel.getLatestContentByTheme(theme: $0)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                                ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                    HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            if !viewModel.eventBannerList.isEmpty {
 | 
			
		||||
                                SectionEventBannerView(items: viewModel.eventBannerList)
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                            if !viewModel.originalAudioDramaList.isEmpty {
 | 
			
		||||
                                VStack(alignment: .leading, spacing: 16) {
 | 
			
		||||
                                    HStack(spacing: 0) {
 | 
			
		||||
                                        Text("오직")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                            .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                            .foregroundColor(.button)
 | 
			
		||||
                                        
 | 
			
		||||
                                        Text(" 보이스온에서만")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                            .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                            .foregroundColor(.white)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.horizontal, 24)
 | 
			
		||||
                                    
 | 
			
		||||
                                    ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                        HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                        }
 | 
			
		||||
                                        .padding(.horizontal, 24)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
@@ -116,84 +124,131 @@ struct HomeTabView: View {
 | 
			
		||||
                            VStack(alignment: .leading, spacing: 16) {
 | 
			
		||||
                                HStack(spacing: 0) {
 | 
			
		||||
                                    Text("요일별")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.button)
 | 
			
		||||
                                    
 | 
			
		||||
                                    Text(" 시리즈")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.white)
 | 
			
		||||
                                }
 | 
			
		||||
                                .padding(.horizontal, 24)
 | 
			
		||||
                                
 | 
			
		||||
                                ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                    HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.horizontal, 24)
 | 
			
		||||
                                }
 | 
			
		||||
                                
 | 
			
		||||
                                ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                    HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.horizontal, 24)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                            VStack(alignment: .leading, spacing: 16) {
 | 
			
		||||
                                HStack(spacing: 0) {
 | 
			
		||||
                                    Text("보온")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.button)
 | 
			
		||||
                                    
 | 
			
		||||
                                    Text(" 주간 차트")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.white)
 | 
			
		||||
                                }
 | 
			
		||||
                                .padding(.horizontal, 24)
 | 
			
		||||
                                
 | 
			
		||||
                                ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                    HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.horizontal, 24)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                            VStack(alignment: .leading, spacing: 16) {
 | 
			
		||||
                                HStack(spacing: 0) {
 | 
			
		||||
                                    Text("추천")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.button)
 | 
			
		||||
                                    
 | 
			
		||||
                                    Text(" 채널")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.white)
 | 
			
		||||
                                }
 | 
			
		||||
                                .padding(.horizontal, 24)
 | 
			
		||||
                                
 | 
			
		||||
                                ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                    HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.horizontal, 24)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                            VStack(alignment: .leading, spacing: 16) {
 | 
			
		||||
                                HStack(spacing: 0) {
 | 
			
		||||
                                    Text("무료")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.button)
 | 
			
		||||
                                    
 | 
			
		||||
                                    Text(" 콘텐츠")
 | 
			
		||||
                                        .font(.custom(Font.bold.rawValue, size: 26))
 | 
			
		||||
                                        .font(.custom(Font.preBold.rawValue, size: 26))
 | 
			
		||||
                                        .foregroundColor(.white)
 | 
			
		||||
                                }
 | 
			
		||||
                                .padding(.horizontal, 24)
 | 
			
		||||
                                
 | 
			
		||||
                                ScrollView(.horizontal, showsIndicators: false) {
 | 
			
		||||
                                    HStack(spacing: 16) {
 | 
			
		||||
                                        
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .padding(.horizontal, 24)
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                            Text("""
 | 
			
		||||
    - 회사명 : 주식회사 소다라이브
 | 
			
		||||
 | 
			
		||||
    - 대표자 : 이재형
 | 
			
		||||
 | 
			
		||||
    - 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호
 | 
			
		||||
 | 
			
		||||
    - 사업자등록번호 : 870-81-03220
 | 
			
		||||
 | 
			
		||||
    - 통신판매업신고 : 제2024-성남분당B-1012호
 | 
			
		||||
 | 
			
		||||
    - 고객센터 : 02.2055.1477 (이용시간 10:00~19:00)
 | 
			
		||||
 | 
			
		||||
    - 대표 이메일 : sodalive.official@gmail.com
 | 
			
		||||
    """)
 | 
			
		||||
                            .font(.custom(Font.medium.rawValue, size: 11))
 | 
			
		||||
                            .foregroundColor(Color.gray77)
 | 
			
		||||
                            .padding(.top, 30)
 | 
			
		||||
                            .padding(.horizontal, 13.3)
 | 
			
		||||
                        }
 | 
			
		||||
                        .padding(.vertical, 24)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
                        .padding(24)
 | 
			
		||||
            .onAppear {
 | 
			
		||||
                viewModel.fetchData()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
 | 
			
		||||
            HStack {
 | 
			
		||||
                Spacer()
 | 
			
		||||
                Text(viewModel.errorMessage)
 | 
			
		||||
                    .padding(.vertical, 13.3)
 | 
			
		||||
                    .frame(width: screenSize().width - 66.7, alignment: .center)
 | 
			
		||||
                    .font(.custom(Font.medium.rawValue, size: 12))
 | 
			
		||||
                    .background(Color.button)
 | 
			
		||||
                    .foregroundColor(Color.white)
 | 
			
		||||
                    .multilineTextAlignment(.leading)
 | 
			
		||||
                    .cornerRadius(20)
 | 
			
		||||
                    .padding(.bottom, 66.7)
 | 
			
		||||
                Spacer()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ final class HomeTabViewModel: ObservableObject {
 | 
			
		||||
    @Published var creatorRanking: [GetExplorerSectionCreatorResponse] = []
 | 
			
		||||
    @Published var latestContentThemeList: [String] = []
 | 
			
		||||
    @Published var latestContentList: [AudioContentMainItem] = []
 | 
			
		||||
    @Published var eventBannerList: GetEventResponse? = nil
 | 
			
		||||
    @Published var eventBannerList: [EventItem] = []
 | 
			
		||||
    @Published var originalAudioDramaList: [SeriesListItem] = []
 | 
			
		||||
    @Published var auditionList: [GetAuditionListItem] = []
 | 
			
		||||
    @Published var dayOfWeekSeriesList: [SeriesListItem] = []
 | 
			
		||||
@@ -32,4 +32,93 @@ final class HomeTabViewModel: ObservableObject {
 | 
			
		||||
    @Published var recommendChannelList: [RecommendChannelResponse] = []
 | 
			
		||||
    @Published var freeContentList: [AudioContentMainItem] = []
 | 
			
		||||
    @Published var curationList: [GetContentCurationResponse] = []
 | 
			
		||||
    
 | 
			
		||||
    func fetchData() {
 | 
			
		||||
        isLoading = true
 | 
			
		||||
        
 | 
			
		||||
        repository.fetchData()
 | 
			
		||||
            .sink { result in
 | 
			
		||||
                switch result {
 | 
			
		||||
                case .finished:
 | 
			
		||||
                    DEBUG_LOG("finish")
 | 
			
		||||
                case .failure(let error):
 | 
			
		||||
                    ERROR_LOG(error.localizedDescription)
 | 
			
		||||
                }
 | 
			
		||||
            } receiveValue: { [unowned self] response in
 | 
			
		||||
                let responseData = response.data
 | 
			
		||||
                
 | 
			
		||||
                do {
 | 
			
		||||
                    let jsonDecoder = JSONDecoder()
 | 
			
		||||
                    let decoded = try jsonDecoder.decode(ApiResponse<GetHomeResponse>.self, from: responseData)
 | 
			
		||||
                    
 | 
			
		||||
                    if let data = decoded.data, decoded.success {
 | 
			
		||||
                        self.liveList = data.liveList
 | 
			
		||||
                        self.creatorRanking = data.creatorRanking
 | 
			
		||||
                        self.latestContentThemeList = ["전체"] + data.latestContentThemeList
 | 
			
		||||
                        self.latestContentList = data.latestContentList
 | 
			
		||||
                        self.eventBannerList = data.eventBannerList.eventList
 | 
			
		||||
                        self.originalAudioDramaList = data.originalAudioDramaList
 | 
			
		||||
                        self.auditionList = data.auditionList
 | 
			
		||||
                        self.dayOfWeekSeriesList = data.dayOfWeekSeriesList
 | 
			
		||||
                        self.contentRanking = data.contentRanking
 | 
			
		||||
                        self.recommendChannelList = data.recommendChannelList
 | 
			
		||||
                        self.freeContentList = data.freeContentList
 | 
			
		||||
                        self.curationList = data.curationList
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if let message = decoded.message {
 | 
			
		||||
                            self.errorMessage = message
 | 
			
		||||
                        } else {
 | 
			
		||||
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        self.isShowPopup = true
 | 
			
		||||
                    }
 | 
			
		||||
                } catch {
 | 
			
		||||
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
			
		||||
                    self.isShowPopup = true
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                self.isLoading = false
 | 
			
		||||
            }
 | 
			
		||||
            .store(in: &subscription)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func getLatestContentByTheme(theme: String) {
 | 
			
		||||
        isLoading = true
 | 
			
		||||
        
 | 
			
		||||
        repository.getLatestContentByTheme(theme: theme == "전체" ? "" : theme)
 | 
			
		||||
            .sink { result in
 | 
			
		||||
                switch result {
 | 
			
		||||
                case .finished:
 | 
			
		||||
                    DEBUG_LOG("finish")
 | 
			
		||||
                case .failure(let error):
 | 
			
		||||
                    ERROR_LOG(error.localizedDescription)
 | 
			
		||||
                }
 | 
			
		||||
            } receiveValue: { [unowned self] response in
 | 
			
		||||
                let responseData = response.data
 | 
			
		||||
                
 | 
			
		||||
                do {
 | 
			
		||||
                    let jsonDecoder = JSONDecoder()
 | 
			
		||||
                    let decoded = try jsonDecoder.decode(ApiResponse<[AudioContentMainItem]>.self, from: responseData)
 | 
			
		||||
                    
 | 
			
		||||
                    if let data = decoded.data, decoded.success {
 | 
			
		||||
                        self.latestContentList = data
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if let message = decoded.message {
 | 
			
		||||
                            self.errorMessage = message
 | 
			
		||||
                        } else {
 | 
			
		||||
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        self.isShowPopup = true
 | 
			
		||||
                    }
 | 
			
		||||
                } catch {
 | 
			
		||||
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
			
		||||
                    self.isShowPopup = true
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                self.isLoading = false
 | 
			
		||||
            }
 | 
			
		||||
            .store(in: &subscription)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -114,30 +114,6 @@ struct LoginView: View {
 | 
			
		||||
                    }
 | 
			
		||||
                    .padding(.top, 20)
 | 
			
		||||
                
 | 
			
		||||
                HStack(spacing: 13.3) {
 | 
			
		||||
                    Image("ic_login_email")
 | 
			
		||||
                        .onTapGesture {
 | 
			
		||||
                            hideKeyboard()
 | 
			
		||||
                            AppState.shared.setAppStep(step: .signUp)
 | 
			
		||||
                        }
 | 
			
		||||
                    
 | 
			
		||||
                    Image("ic_login_kakao")
 | 
			
		||||
                        .onTapGesture {
 | 
			
		||||
                            hideKeyboard()
 | 
			
		||||
                        }
 | 
			
		||||
                    
 | 
			
		||||
                    Image("ic_login_google")
 | 
			
		||||
                        .onTapGesture {
 | 
			
		||||
                            hideKeyboard()
 | 
			
		||||
                        }
 | 
			
		||||
                    
 | 
			
		||||
                    Image("ic_login_apple")
 | 
			
		||||
                        .onTapGesture {
 | 
			
		||||
                            hideKeyboard()
 | 
			
		||||
                        }
 | 
			
		||||
                }
 | 
			
		||||
                .padding(.top, 20)
 | 
			
		||||
                
 | 
			
		||||
                Spacer()
 | 
			
		||||
            }
 | 
			
		||||
            .onAppear {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user