feat: 메인 홈

- 요일별 시리즈, 오디션 추가
This commit is contained in:
Yu Sung
2025-07-12 01:20:02 +09:00
parent 5a9b95c2bf
commit 6a9854bdd7
6 changed files with 459 additions and 50 deletions

View File

@@ -0,0 +1,172 @@
//
// DayOfWeekSeriesView.swift
// SodaLive
//
// Created by klaus on 7/11/25.
//
import SwiftUI
struct DayOfWeek {
let dayOfWeekStr: String
let dayOfWeek: SeriesPublishedDaysOfWeek
}
struct DayOfWeekSeriesView: View {
let seriesList: [SeriesListItem]
let onTapDayOfWeek: (SeriesPublishedDaysOfWeek) -> Void
@State private var dayOfWeek: SeriesPublishedDaysOfWeek = .FRI
private let dayOfWeekItems: [DayOfWeek] = [
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .MON),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .TUE),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .WED),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .THU),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .FRI),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .SAT),
DayOfWeek(dayOfWeekStr: "", dayOfWeek: .SUN),
DayOfWeek(dayOfWeekStr: "랜덤", dayOfWeek: .RANDOM),
]
//
private let dayOfWeeks: [SeriesPublishedDaysOfWeek] = [
.RANDOM,
.SUN,
.MON,
.TUE,
.WED,
.THU,
.FRI,
.SAT
]
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 0) {
Text("요일별")
.font(.custom(Font.preBold.rawValue, size: 26))
.foregroundColor(.button)
Text(" 시리즈")
.font(.custom(Font.preBold.rawValue, size: 26))
.foregroundColor(.white)
}
.padding(.horizontal, 24)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 5) {
ForEach(0..<dayOfWeekItems.count, id: \.self) {
let item = dayOfWeekItems[$0]
DayOfWeekDayView(dayOfWeek: item.dayOfWeekStr, isSelected: dayOfWeek == item.dayOfWeek)
.onTapGesture {
if dayOfWeek != item.dayOfWeek {
dayOfWeek = item.dayOfWeek
onTapDayOfWeek(item.dayOfWeek)
}
}
}
}
.padding(.horizontal, 24)
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(0..<seriesList.count, id: \.self) {
SeriesItemView(item: seriesList[$0])
}
}
.padding(.horizontal, 24)
}
}
.onAppear {
dayOfWeek = dayOfWeeks[Calendar.current.component(.weekday, from: Date())]
}
}
}
struct DayOfWeekDayView: View {
let dayOfWeek: String
let isSelected: Bool
var body: some View {
Text(dayOfWeek)
.font(
.custom(
isSelected ? Font.preBold.rawValue : Font.preRegular.rawValue,
size: 18
)
)
.foregroundColor(.white)
.padding(.vertical, 8)
.padding(.horizontal, 11)
.background(Color(hex: "263238").cornerRadius(16))
.overlay {
RoundedRectangle(cornerRadius: 16)
.strokeBorder(
LinearGradient(
stops: [
.init(color: Color.main, location: 0.24),
.init(color: Color(hex: "6D5ED7"), location: 1.0)
],
startPoint: .top,
endPoint: .bottom
),
lineWidth: isSelected ? 4 : 0
)
}
}
}
#Preview {
DayOfWeekSeriesView(
seriesList: [
SeriesListItem(
seriesId: 1,
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
publishedDaysOfWeek: "매주 수, 토요일",
isComplete: true,
creator: SeriesListItemCreator(
creatorId: 1,
nickname: "creator",
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
),
numberOfContent: 10,
isNew: true,
isPopular: true
),
SeriesListItem(
seriesId: 2,
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
publishedDaysOfWeek: "매주 수, 토요일",
isComplete: false,
creator: SeriesListItemCreator(
creatorId: 1,
nickname: "creator",
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
),
numberOfContent: 10,
isNew: false,
isPopular: true
),
SeriesListItem(
seriesId: 1,
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
publishedDaysOfWeek: "매주 수, 토요일",
isComplete: false,
creator: SeriesListItemCreator(
creatorId: 1,
nickname: "creator",
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
),
numberOfContent: 10,
isNew: true,
isPopular: false
)
]
) { _ in }
}

View File

@@ -6,13 +6,126 @@
//
import SwiftUI
import Kingfisher
struct SeriesItemView: View {
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
let item: SeriesListItem
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
VStack(alignment: .leading, spacing: 4) {
ZStack {
KFImage(URL(string: item.coverImage))
.cancelOnDisappear(true)
.resizable()
.scaledToFill()
.frame(width: 168, height: 238, alignment: .center)
.cornerRadius(16)
.clipped()
.onTapGesture {
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
AppState.shared
.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
} else {
AppState.shared
.setAppStep(step: .login)
}
}
VStack(alignment: .leading, spacing: 0) {
if !item.isComplete && item.isNew {
Text("신작")
.font(.custom(Font.preRegular.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])
} else if item.isPopular {
Text("인기")
.font(.custom(Font.preRegular.rawValue, size: 12))
.foregroundColor(.white)
.padding(.horizontal, 10)
.padding(.vertical, 3)
.background(Color(hex: "EF5247"))
.cornerRadius(16, corners: [.topLeft, .bottomRight])
}
Spacer()
HStack {
Spacer()
if item.isComplete {
Text("완결")
.font(.custom(Font.preRegular.rawValue, size: 12))
.foregroundColor(.white)
.padding(.horizontal, 10)
.padding(.vertical, 3)
.background(Color.black.opacity(0.7))
.cornerRadius(16, corners: [.topLeft, .bottomRight])
} else {
Text("\(item.numberOfContent)")
.font(.custom(Font.preRegular.rawValue, size: 12))
.foregroundColor(.white)
.padding(.horizontal, 10)
.padding(.vertical, 3)
.overlay {
RoundedRectangle(cornerRadius: 39)
.strokeBorder(lineWidth: 1)
.foregroundColor(.white)
}
}
}
.padding(.bottom, 8)
.padding(.trailing, 8)
}
}
.frame(width: 168, height: 238, alignment: .center)
Text(item.title)
.font(.custom(Font.preRegular.rawValue, size: 18))
.foregroundColor(Color(hex: "B0BEC5"))
.lineLimit(1)
.padding(.horizontal, 8)
Text(item.creator.nickname)
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "78909C"))
.lineLimit(1)
.padding(.horizontal, 8)
}
.frame(width: 168)
}
}
#Preview {
SeriesItemView()
SeriesItemView(
item: SeriesListItem(
seriesId: 1,
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
publishedDaysOfWeek: "매주 수, 토요일",
isComplete: false,
creator: SeriesListItemCreator(
creatorId: 1,
nickname: "creator",
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
),
numberOfContent: 10,
isNew: true,
isPopular: false
)
)
}

View File

@@ -0,0 +1,116 @@
//
// HomeAuditionView.swift
// SodaLive
//
// Created by klaus on 7/12/25.
//
import SwiftUI
import Kingfisher
struct HomeAuditionView: View {
@State private var currentIndex = 0
@State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
let items: [GetAuditionListItem]
var body: some View {
VStack(spacing: 16) {
TabView(selection: $currentIndex) {
ForEach(0..<items.count, id: \.self) { index in
let item = items[index]
if let url = item.imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
KFImage(URL(string: url))
.cancelOnDisappear(true)
.resizable()
.scaledToFill()
.frame(
width: screenSize().width - 48,
height: (screenSize().width - 48) * 530 / 1000,
alignment: .center
)
.tag(index)
.onTapGesture {
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
AppState.shared.setAppStep(step: .auditionDetail(auditionId: item.id))
} else {
AppState.shared.setAppStep(step: .login)
}
}
} else {
KFImage(URL(string: item.imageUrl))
.cancelOnDisappear(true)
.resizable()
.scaledToFill()
.frame(
width: screenSize().width - 48,
height: (screenSize().width - 48) * 530 / 1000,
alignment: .center
)
.tag(index)
.onTapGesture {
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
AppState.shared.setAppStep(step: .auditionDetail(auditionId: item.id))
} else {
AppState.shared.setAppStep(step: .login)
}
}
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(
width: screenSize().width,
height: screenSize().width * 530 / 1000,
alignment: .center
)
HStack(spacing: 8) {
ForEach(0..<items.count, id: \.self) { index in
Capsule()
.foregroundColor(index == currentIndex ? .button : .gray77)
.frame(width: 10, height: 10)
.tag(index)
}
}
}
.onAppear {
timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
}
.onDisappear {
timer.upstream.connect().cancel()
}
.onReceive(timer) { _ in
DispatchQueue.main.async {
withAnimation {
if currentIndex == items.count - 1 {
currentIndex = 0
} else {
currentIndex += 1
}
}
}
}
}
}
#Preview {
HomeAuditionView(
items: [
GetAuditionListItem(
id: 1,
title: "오디션 제목 테스트",
imageUrl: "https://test-cf.sodalive.net/audition/production/1/audition-2950c964-b460-4085-87e4-f76849c826ab-240-1735215756152",
isOff: false
),
GetAuditionListItem(
id: 3,
title: "스위치온",
imageUrl: "https://test-cf.sodalive.net/audition/production/3/audition-aa934579-c01a-4da2-89fd-cce70d51c612-4267-1735908116928",
isOff: false
)
]
)
}

View File

@@ -115,37 +115,21 @@ struct HomeTabView: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(0..<viewModel.originalAudioDramaList.count, id: \.self) {
SeriesItemView(item: viewModel.originalAudioDramaList[$0])
}
}
.padding(.horizontal, 24)
}
}
}
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 0) {
Text("요일별")
.font(.custom(Font.preBold.rawValue, size: 26))
.foregroundColor(.button)
if !viewModel.auditionList.isEmpty {
HomeAuditionView(items: viewModel.auditionList)
}
Text(" 시리즈")
.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)
}
DayOfWeekSeriesView(seriesList: viewModel.dayOfWeekSeriesList) {
viewModel.getDayOfWeekSeriesList(dayOfWeek: $0)
}
VStack(alignment: .leading, spacing: 16) {

View File

@@ -121,4 +121,43 @@ final class HomeTabViewModel: ObservableObject {
}
.store(in: &subscription)
}
func getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek) {
isLoading = true
repository.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
.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<[SeriesListItem]>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.dayOfWeekSeriesList = 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)
}
}

View File

@@ -17,24 +17,18 @@ struct SectionEventBannerView: View {
let items: [EventItem]
var body: some View {
VStack(spacing: 13.3) {
VStack(spacing: 16) {
TabView(selection: $currentIndex) {
ForEach(0..<items.count, id: \.self) { index in
let item = items[index]
if let url = item.thumbnailImageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
KFImage(URL(string: url))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: screenSize().width,
height: screenSize().width * 300 / 1000
)
)
.resizable()
.scaledToFill()
.frame(
width: screenSize().width,
height: screenSize().width * 300 / 1000,
width: screenSize().width - 48,
height: (screenSize().width - 48) * 300 / 1000,
alignment: .center
)
.tag(index)
@@ -52,17 +46,11 @@ struct SectionEventBannerView: View {
} else {
KFImage(URL(string: item.thumbnailImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: screenSize().width,
height: screenSize().width * 300 / 1000
)
)
.resizable()
.scaledToFill()
.frame(
width: screenSize().width,
height: screenSize().width * 300 / 1000,
width: screenSize().width - 48,
height: (screenSize().width - 48) * 300 / 1000,
alignment: .center
)
.tag(index)
@@ -87,14 +75,11 @@ struct SectionEventBannerView: View {
alignment: .center
)
HStack(spacing: 4) {
HStack(spacing: 8) {
ForEach(0..<items.count, id: \.self) { index in
Capsule()
.foregroundColor(index == currentIndex ? .button : .gray90)
.frame(
width: index == currentIndex ? 18 : 6,
height: 6
)
.foregroundColor(index == currentIndex ? .button : .gray77)
.frame(width: 10, height: 10)
.tag(index)
}
}