시리즈 상세 추가
This commit is contained in:
		| @@ -233,6 +233,15 @@ | |||||||
|         "revision" : "d5a7d856655d5c91f891c2b69d982c30fd5c7bdf", |         "revision" : "d5a7d856655d5c91f891c2b69d982c30fd5c7bdf", | ||||||
|         "version" : "2.1.0" |         "version" : "2.1.0" | ||||||
|       } |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "identity" : "taglayoutview", | ||||||
|  |       "kind" : "remoteSourceControl", | ||||||
|  |       "location" : "https://github.com/yotsu12/TagLayoutView", | ||||||
|  |       "state" : { | ||||||
|  |         "branch" : "master", | ||||||
|  |         "revision" : "815deadaca2b65edb03ec2fe25d0ce300d2eb7b3" | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   "version" : 2 |   "version" : 2 | ||||||
|   | |||||||
| @@ -0,0 +1,140 @@ | |||||||
|  | // | ||||||
|  | //  SeriesContentListItemView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/30/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import Kingfisher | ||||||
|  |  | ||||||
|  | struct SeriesContentListItemView: View { | ||||||
|  |      | ||||||
|  |     let item: GetSeriesContentListItem | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(spacing: 12) { | ||||||
|  |             HStack(spacing: 11) { | ||||||
|  |                 KFImage(URL(string: item.coverImage)) | ||||||
|  |                     .resizable() | ||||||
|  |                     .scaledToFit() | ||||||
|  |                     .frame(width: 66.7, height: 66.7) | ||||||
|  |                     .cornerRadius(5.3) | ||||||
|  |                  | ||||||
|  |                 VStack(alignment: .leading, spacing: 2.7) { | ||||||
|  |                     Text(item.duration) | ||||||
|  |                         .font(.custom(Font.medium.rawValue, size: 10)) | ||||||
|  |                         .foregroundColor(Color.gray77) | ||||||
|  |                         .padding(2.7) | ||||||
|  |                         .background(Color.gray22) | ||||||
|  |                         .cornerRadius(2.6) | ||||||
|  |                      | ||||||
|  |                     Text(item.title) | ||||||
|  |                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                         .foregroundColor(Color.grayd2) | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 Spacer() | ||||||
|  |                  | ||||||
|  |                 if item.isOwned { | ||||||
|  |                     Text("소장중") | ||||||
|  |                         .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||||
|  |                         .foregroundColor(Color.gray11) | ||||||
|  |                         .padding(.horizontal, 5.3) | ||||||
|  |                         .padding(.vertical, 2.7) | ||||||
|  |                         .background(Color(hex: "b1ef2c")) | ||||||
|  |                         .cornerRadius(2.6) | ||||||
|  |                 } else if item.isRented { | ||||||
|  |                     Text("대여중") | ||||||
|  |                         .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||||
|  |                         .foregroundColor(Color.white) | ||||||
|  |                         .padding(.horizontal, 5.3) | ||||||
|  |                         .padding(.vertical, 2.7) | ||||||
|  |                         .background(Color(hex: "660fd4")) | ||||||
|  |                         .cornerRadius(2.6) | ||||||
|  |                 } else if item.price > 0 { | ||||||
|  |                     HStack(spacing: 5.3) { | ||||||
|  |                         Image("ic_can") | ||||||
|  |                          | ||||||
|  |                         Text("\(item.price)") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||||
|  |                             .foregroundColor(Color(hex: "909090")) | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Text("무료") | ||||||
|  |                         .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||||
|  |                         .foregroundColor(Color.white) | ||||||
|  |                         .padding(.horizontal, 5.3) | ||||||
|  |                         .padding(.vertical, 2.7) | ||||||
|  |                         .background(Color(hex: "cf5c37")) | ||||||
|  |                         .cornerRadius(2.6) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             Rectangle() | ||||||
|  |                 .foregroundColor(Color.grayd8) | ||||||
|  |                 .frame(maxWidth: .infinity) | ||||||
|  |                 .frame(height: 1) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview("무료") { | ||||||
|  |     SeriesContentListItemView( | ||||||
|  |         item: GetSeriesContentListItem( | ||||||
|  |             contentId: 1, | ||||||
|  |             title: "[무료] 두근두근 연애 연구부 EP1", | ||||||
|  |             coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |             releaseDate: "", | ||||||
|  |             duration: "00:14:59", | ||||||
|  |             price: 0, | ||||||
|  |             isRented: false, | ||||||
|  |             isOwned: false | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview("유료") { | ||||||
|  |     SeriesContentListItemView( | ||||||
|  |         item: GetSeriesContentListItem( | ||||||
|  |             contentId: 1, | ||||||
|  |             title: "두근두근 연애 연구부 EP1", | ||||||
|  |             coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |             releaseDate: "", | ||||||
|  |             duration: "00:14:59", | ||||||
|  |             price: 100, | ||||||
|  |             isRented: false, | ||||||
|  |             isOwned: false | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview("대여") { | ||||||
|  |     SeriesContentListItemView( | ||||||
|  |         item: GetSeriesContentListItem( | ||||||
|  |             contentId: 1, | ||||||
|  |             title: "두근두근 연애 연구부 EP1", | ||||||
|  |             coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |             releaseDate: "", | ||||||
|  |             duration: "00:14:59", | ||||||
|  |             price: 200, | ||||||
|  |             isRented: true, | ||||||
|  |             isOwned: false | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview("소장") { | ||||||
|  |     SeriesContentListItemView( | ||||||
|  |         item: GetSeriesContentListItem( | ||||||
|  |             contentId: 1, | ||||||
|  |             title: "두근두근 연애 연구부 EP1", | ||||||
|  |             coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |             releaseDate: "", | ||||||
|  |             duration: "00:14:59", | ||||||
|  |             price: 300, | ||||||
|  |             isRented: false, | ||||||
|  |             isOwned: true | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | // | ||||||
|  | //  GetSeriesContentListResponse.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/29/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | struct GetSeriesContentListResponse: Decodable { | ||||||
|  |     let totalCount: Int | ||||||
|  |     let items: [GetSeriesContentListItem] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct GetSeriesContentListItem: Decodable { | ||||||
|  |     let contentId: Int | ||||||
|  |     let title: String | ||||||
|  |     let coverImage: String | ||||||
|  |     let releaseDate: String | ||||||
|  |     let duration: String | ||||||
|  |     let price: Int | ||||||
|  |     let isRented: Bool | ||||||
|  |     let isOwned: Bool | ||||||
|  | } | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | // | ||||||
|  | //  GetSeriesDetailResponse.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/29/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | struct GetSeriesDetailResponse: Decodable { | ||||||
|  |     let seriesId: Int | ||||||
|  |     let title: String | ||||||
|  |     let coverImage: String | ||||||
|  |     let introduction: String | ||||||
|  |     let genre: String | ||||||
|  |     let isAdult: Bool | ||||||
|  |     let writer: String? | ||||||
|  |     let studio: String? | ||||||
|  |     let publishedDate: String | ||||||
|  |     let creator: GetSeriesDetailCreator | ||||||
|  |     let rentalMinPrice: Int | ||||||
|  |     let rentalMaxPrice: Int | ||||||
|  |     let rentalPeriod: Int | ||||||
|  |     let minPrice: Int | ||||||
|  |     let maxPrice: Int | ||||||
|  |     let keywordList: [String] | ||||||
|  |     let publishedDaysOfWeek: String | ||||||
|  |     let contentList: [GetSeriesContentListItem] | ||||||
|  |     let contentCount: Int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct GetSeriesDetailCreator: Decodable { | ||||||
|  |     let creatorId: Int | ||||||
|  |     let nickname: String | ||||||
|  |     let profileImage: String | ||||||
|  |     let isFollow: Bool | ||||||
|  | } | ||||||
| @@ -0,0 +1,106 @@ | |||||||
|  | // | ||||||
|  | //  SeriesDetailHomeView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/29/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct SeriesDetailHomeView: View { | ||||||
|  |      | ||||||
|  |     let title: String | ||||||
|  |     let seriesId: Int | ||||||
|  |     let contentCount: Int | ||||||
|  |     let contentList: [GetSeriesContentListItem] | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(spacing: 0) { | ||||||
|  |             HStack(spacing: 0) { | ||||||
|  |                 Text("전체회차 듣기") | ||||||
|  |                     .font(.custom(Font.bold.rawValue, size: 16)) | ||||||
|  |                     .foregroundColor(Color.button) | ||||||
|  |                  | ||||||
|  |                 Text(" (\(contentCount))") | ||||||
|  |                     .font(.custom(Font.light.rawValue, size: 16)) | ||||||
|  |                     .foregroundColor(Color.button) | ||||||
|  |             } | ||||||
|  |             .frame(maxWidth: .infinity) | ||||||
|  |             .padding(.vertical, 13.3) | ||||||
|  |             .background(Color.bg) | ||||||
|  |             .cornerRadius(5.3) | ||||||
|  |             .overlay( | ||||||
|  |                 RoundedRectangle(cornerRadius: 5.3) | ||||||
|  |                     .stroke() | ||||||
|  |                     .foregroundColor(Color.button) | ||||||
|  |             ) | ||||||
|  |             .padding(.top, 16) | ||||||
|  |             .onTapGesture {} | ||||||
|  |              | ||||||
|  |             VStack(spacing: 8) { | ||||||
|  |                 ForEach(0..<contentList.count, id: \.self) { | ||||||
|  |                     let item = contentList[$0] | ||||||
|  |                      | ||||||
|  |                     SeriesContentListItemView(item: item) | ||||||
|  |                         .contentShape(Rectangle()) | ||||||
|  |                         .onTapGesture { | ||||||
|  |                             AppState.shared | ||||||
|  |                                 .setAppStep(step: .contentDetail(contentId: item.contentId)) | ||||||
|  |                         } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding(.top, 16) | ||||||
|  |         } | ||||||
|  |         .padding(.horizontal, 13.3) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     SeriesDetailHomeView( | ||||||
|  |         title: "변호사 우영우", | ||||||
|  |         seriesId: 0, | ||||||
|  |         contentCount: 10, | ||||||
|  |         contentList: [ | ||||||
|  |             GetSeriesContentListItem( | ||||||
|  |                 contentId: 1, | ||||||
|  |                 title: "[무료] 두근두근 연애 연구부 EP1", | ||||||
|  |                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 releaseDate: "", | ||||||
|  |                 duration: "00:14:59", | ||||||
|  |                 price: 0, | ||||||
|  |                 isRented: false, | ||||||
|  |                 isOwned: false | ||||||
|  |             ), | ||||||
|  |             GetSeriesContentListItem( | ||||||
|  |                 contentId: 2, | ||||||
|  |                 title: "두근두근 연애 연구부 EP2", | ||||||
|  |                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 releaseDate: "", | ||||||
|  |                 duration: "00:14:59", | ||||||
|  |                 price: 100, | ||||||
|  |                 isRented: false, | ||||||
|  |                 isOwned: false | ||||||
|  |             ), | ||||||
|  |             GetSeriesContentListItem( | ||||||
|  |                 contentId: 3, | ||||||
|  |                 title: "두근두근 연애 연구부 EP3", | ||||||
|  |                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 releaseDate: "", | ||||||
|  |                 duration: "00:14:59", | ||||||
|  |                 price: 100, | ||||||
|  |                 isRented: true, | ||||||
|  |                 isOwned: false | ||||||
|  |             ), | ||||||
|  |             GetSeriesContentListItem( | ||||||
|  |                 contentId: 4, | ||||||
|  |                 title: "두근두근 연애 연구부 EP4", | ||||||
|  |                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 releaseDate: "", | ||||||
|  |                 duration: "00:14:59", | ||||||
|  |                 price: 100, | ||||||
|  |                 isRented: false, | ||||||
|  |                 isOwned: true | ||||||
|  |             ) | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1,165 @@ | |||||||
|  | // | ||||||
|  | //  SeriesDetailIntroductionView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/29/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import TagLayoutView | ||||||
|  |  | ||||||
|  | struct SeriesDetailIntroductionView: View { | ||||||
|  |      | ||||||
|  |     let width: CGFloat | ||||||
|  |     let seriesDetail: GetSeriesDetailResponse | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(alignment: .leading, spacing: 16) { | ||||||
|  |             Text("키워드") | ||||||
|  |                 .font(.custom(Font.bold.rawValue, size: 14.7)) | ||||||
|  |                 .foregroundColor(Color.grayee) | ||||||
|  |                 .padding(.top, 16) | ||||||
|  |                 .padding(.horizontal, 13.3) | ||||||
|  |              | ||||||
|  |             TagLayoutView( | ||||||
|  |                 seriesDetail.keywordList, | ||||||
|  |                 tagFont: UIFont(name: Font.medium.rawValue, size: 12)!, | ||||||
|  |                 padding: 5.3, | ||||||
|  |                 parentWidth: width | ||||||
|  |             ) { | ||||||
|  |                 SeriesKeywordChipView(keyword: $0) | ||||||
|  |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|  |              | ||||||
|  |             Rectangle() | ||||||
|  |                 .frame(height: 6.7) | ||||||
|  |                 .foregroundColor(Color.gray22) | ||||||
|  |              | ||||||
|  |             VStack(alignment: .leading, spacing: 13.3) { | ||||||
|  |                 Text("작품소개") | ||||||
|  |                     .font(.custom(Font.bold.rawValue, size: 14.7)) | ||||||
|  |                     .foregroundColor(Color.grayee) | ||||||
|  |                  | ||||||
|  |                 Text(seriesDetail.introduction) | ||||||
|  |                     .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                     .foregroundColor(Color.gray77) | ||||||
|  |                     .lineSpacing(4) | ||||||
|  |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|  |              | ||||||
|  |             Rectangle() | ||||||
|  |                 .frame(height: 6.7) | ||||||
|  |                 .foregroundColor(Color.gray22) | ||||||
|  |              | ||||||
|  |             VStack(alignment: .leading, spacing: 16) { | ||||||
|  |                 Text("상세정보") | ||||||
|  |                     .font(.custom(Font.bold.rawValue, size: 14.7)) | ||||||
|  |                     .foregroundColor(Color.grayee) | ||||||
|  |                  | ||||||
|  |                 HStack(spacing: 30) { | ||||||
|  |                     VStack(alignment: .leading, spacing: 13.3) { | ||||||
|  |                         Text("장르") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.gray77) | ||||||
|  |                          | ||||||
|  |                         Text("연령제한") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.gray77) | ||||||
|  |                          | ||||||
|  |                         if let _ = seriesDetail.writer { | ||||||
|  |                             Text("작가") | ||||||
|  |                                 .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                                 .foregroundColor(Color.gray77) | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         if let _ = seriesDetail.studio { | ||||||
|  |                             Text("제작사") | ||||||
|  |                                 .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                                 .foregroundColor(Color.gray77) | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         Text("연재") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.gray77) | ||||||
|  |                          | ||||||
|  |                         Text("출시일") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.gray77) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     VStack(alignment: .leading, spacing: 13.3) { | ||||||
|  |                         Text(seriesDetail.genre) | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.white) | ||||||
|  |                          | ||||||
|  |                         Text(seriesDetail.isAdult ? "19세 이상" : "전체연령가") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.white) | ||||||
|  |                          | ||||||
|  |                         if let writer = seriesDetail.writer { | ||||||
|  |                             Text(writer) | ||||||
|  |                                 .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                                 .foregroundColor(Color.white) | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         if let studio = seriesDetail.studio { | ||||||
|  |                             Text(studio) | ||||||
|  |                                 .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                                 .foregroundColor(Color.white) | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         Text(seriesDetail.publishedDaysOfWeek == "랜덤" ? seriesDetail.publishedDaysOfWeek : "\(seriesDetail.publishedDaysOfWeek)요일") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.white) | ||||||
|  |                          | ||||||
|  |                         Text(seriesDetail.publishedDate) | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.white) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|  |              | ||||||
|  |             VStack(alignment: .leading, spacing: 13.3) { | ||||||
|  |                 Text("가격") | ||||||
|  |                     .font(.custom(Font.bold.rawValue, size: 14.7)) | ||||||
|  |                     .foregroundColor(Color.grayee) | ||||||
|  |                  | ||||||
|  |                 HStack(spacing: 30) { | ||||||
|  |                     VStack(alignment: .leading, spacing: 13.3) { | ||||||
|  |                         Text("대여") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.gray77) | ||||||
|  |                          | ||||||
|  |                         Text("소장") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.gray77) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     VStack(alignment: .leading, spacing: 13.3) { | ||||||
|  |                         Text("\(calculatePriceInfo(seriesDetail.rentalMinPrice, seriesDetail.rentalMaxPrice)) (15일)") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.button) | ||||||
|  |                          | ||||||
|  |                         Text("\(calculatePriceInfo(seriesDetail.minPrice, seriesDetail.maxPrice))") | ||||||
|  |                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                             .foregroundColor(Color.button) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func calculatePriceInfo(_ minPrice: Int, _ maxPrice: Int) -> String { | ||||||
|  |         if minPrice == maxPrice { | ||||||
|  |             if maxPrice == 0 { | ||||||
|  |                 return "무료" | ||||||
|  |             } else { | ||||||
|  |                 return "\(maxPrice)" | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             return "\(minPrice == 0 ? "무료" : "\(minPrice)") ~ \(maxPrice)캔" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										195
									
								
								SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | |||||||
|  | // | ||||||
|  | //  SeriesDetailView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/29/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import Kingfisher | ||||||
|  |  | ||||||
|  | struct SeriesDetailView: View { | ||||||
|  |      | ||||||
|  |     @ObservedObject var viewModel = SeriesDetailViewModel() | ||||||
|  |      | ||||||
|  |     let seriesId: Int | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         BaseView(isLoading: $viewModel.isLoading) { | ||||||
|  |             ZStack(alignment: .top) { | ||||||
|  |                 Color.gray11.ignoresSafeArea() | ||||||
|  |                  | ||||||
|  |                 if let seriesDetail = viewModel.seriesDetail { | ||||||
|  |                     KFImage(URL(string: seriesDetail.coverImage)) | ||||||
|  |                         .resizable() | ||||||
|  |                         .scaledToFit() | ||||||
|  |                         .frame(maxWidth: .infinity) | ||||||
|  |                         .blur(radius: 25) | ||||||
|  |                      | ||||||
|  |                     ScrollView(.vertical, showsIndicators: false) { | ||||||
|  |                         VStack(spacing: 0) { | ||||||
|  |                             HStack(spacing: 0) { | ||||||
|  |                                 Image("ic_back") | ||||||
|  |                                     .resizable() | ||||||
|  |                                     .frame(width: 20, height: 20) | ||||||
|  |                                     .onTapGesture { AppState.shared.back() } | ||||||
|  |                                  | ||||||
|  |                                 Spacer() | ||||||
|  |                             } | ||||||
|  |                             .padding(.horizontal, 13.3) | ||||||
|  |                             .frame(height: 50) | ||||||
|  |                              | ||||||
|  |                             ZStack { | ||||||
|  |                                 Rectangle() | ||||||
|  |                                     .frame(width: screenSize().width, height: 94) | ||||||
|  |                                     .foregroundColor(Color.gray11) | ||||||
|  |                                     .cornerRadius(21.3, corners: [.topLeft, .topRight]) | ||||||
|  |                                     .padding(.top, 94) | ||||||
|  |                                  | ||||||
|  |                                 KFImage(URL(string: seriesDetail.coverImage)) | ||||||
|  |                                     .resizable() | ||||||
|  |                                     .scaledToFit() | ||||||
|  |                                     .cornerRadius(5) | ||||||
|  |                                     .frame(width: 133.3, height: 188) | ||||||
|  |                                  | ||||||
|  |                             } | ||||||
|  |                              | ||||||
|  |                             VStack(alignment: .leading, spacing: 0) { | ||||||
|  |                                 Text(seriesDetail.title) | ||||||
|  |                                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||||
|  |                                     .foregroundColor(Color.grayee) | ||||||
|  |                                     .padding(.horizontal, 13.3) | ||||||
|  |                                     .padding(.top, 24) | ||||||
|  |                                  | ||||||
|  |                                 HStack(spacing: 5.3) { | ||||||
|  |                                     Text(seriesDetail.genre) | ||||||
|  |                                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                                         .foregroundColor(Color(hex: "3bac6a")) | ||||||
|  |                                         .padding(.horizontal, 5.3) | ||||||
|  |                                         .padding(.vertical, 3.3) | ||||||
|  |                                         .background(Color(hex: "28312b")) | ||||||
|  |                                         .cornerRadius(2.6) | ||||||
|  |                                      | ||||||
|  |                                     if seriesDetail.isAdult { | ||||||
|  |                                         Text("19세") | ||||||
|  |                                             .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                                             .foregroundColor(Color(hex: "f1291c")) | ||||||
|  |                                             .padding(.horizontal, 5.3) | ||||||
|  |                                             .padding(.vertical, 3.3) | ||||||
|  |                                             .background(Color(hex: "312827")) | ||||||
|  |                                             .cornerRadius(2.6) | ||||||
|  |                                     } else { | ||||||
|  |                                         Text("전체연령가") | ||||||
|  |                                             .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                                             .foregroundColor(Color(hex: "d2d2d2")) | ||||||
|  |                                             .padding(.horizontal, 5.3) | ||||||
|  |                                             .padding(.vertical, 3.3) | ||||||
|  |                                             .background(Color(hex: "222222")) | ||||||
|  |                                             .cornerRadius(2.6) | ||||||
|  |                                     } | ||||||
|  |                                      | ||||||
|  |                                     Text(seriesDetail.publishedDaysOfWeek) | ||||||
|  |                                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                                         .foregroundColor(Color(hex: "909090")) | ||||||
|  |                                 } | ||||||
|  |                                 .padding(.top, 8) | ||||||
|  |                                 .padding(.horizontal, 13.3) | ||||||
|  |                                  | ||||||
|  |                                 ScrollView(.horizontal, showsIndicators: false) { | ||||||
|  |                                     HStack(spacing: 5.3) { | ||||||
|  |                                         ForEach(0..<seriesDetail.keywordList.count, id: \.self) { | ||||||
|  |                                             SeriesKeywordChipView(keyword: seriesDetail.keywordList[$0]) | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                                 .padding(.top, 16) | ||||||
|  |                                 .padding(.horizontal, 13.3) | ||||||
|  |                                  | ||||||
|  |                                 HStack(spacing: 5.3) { | ||||||
|  |                                     KFImage(URL(string: seriesDetail.creator.profileImage)) | ||||||
|  |                                         .resizable() | ||||||
|  |                                         .scaledToFit() | ||||||
|  |                                         .clipShape(Circle()) | ||||||
|  |                                         .frame(width: 26.7, height: 26.7) | ||||||
|  |                                      | ||||||
|  |                                     Text(seriesDetail.creator.nickname) | ||||||
|  |                                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                                         .foregroundColor(Color(hex: "909090")) | ||||||
|  |                                      | ||||||
|  |                                     Spacer() | ||||||
|  |                                      | ||||||
|  |                                     if seriesDetail.creator.creatorId != UserDefaults.int(forKey: .userId) { | ||||||
|  |                                         Image(viewModel.isFollow ? "btn_following_big" : "btn_follow_big") | ||||||
|  |                                             .onTapGesture { | ||||||
|  |                                                 if viewModel.isFollow { | ||||||
|  |                                                     viewModel.unFollow(seriesDetail.creator.creatorId) | ||||||
|  |                                                 } else { | ||||||
|  |                                                     viewModel.follow(seriesDetail.creator.creatorId) | ||||||
|  |                                                 } | ||||||
|  |                                             } | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                                 .padding(.top, 16) | ||||||
|  |                                 .padding(.horizontal, 13.3) | ||||||
|  |                                  | ||||||
|  |                                 HStack(spacing: 0) { | ||||||
|  |                                     SeriesDetailTabView( | ||||||
|  |                                         title: "홈", | ||||||
|  |                                         width: screenSize().width / 2, | ||||||
|  |                                         isSelected: viewModel.currentTab == .home | ||||||
|  |                                     ) { | ||||||
|  |                                         if viewModel.currentTab != .home { | ||||||
|  |                                             viewModel.currentTab = .home | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                      | ||||||
|  |                                     SeriesDetailTabView( | ||||||
|  |                                         title: "작품소개", | ||||||
|  |                                         width: screenSize().width / 2, | ||||||
|  |                                         isSelected: viewModel.currentTab == .introduction | ||||||
|  |                                     ) { | ||||||
|  |                                         if viewModel.currentTab != .introduction { | ||||||
|  |                                             viewModel.currentTab = .introduction | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                                 .padding(.top, 16) | ||||||
|  |                                  | ||||||
|  |                                 Rectangle() | ||||||
|  |                                     .foregroundColor(Color.gray90.opacity(0.5)) | ||||||
|  |                                     .frame(height: 1) | ||||||
|  |                                     .frame(maxWidth: .infinity) | ||||||
|  |                                  | ||||||
|  |                                 switch(viewModel.currentTab) { | ||||||
|  |                                 case .introduction: | ||||||
|  |                                     SeriesDetailIntroductionView( | ||||||
|  |                                         width: screenSize().width - 26.7, | ||||||
|  |                                         seriesDetail: seriesDetail | ||||||
|  |                                     ) | ||||||
|  |                                      | ||||||
|  |                                 default: | ||||||
|  |                                     SeriesDetailHomeView( | ||||||
|  |                                         title: seriesDetail.title, | ||||||
|  |                                         seriesId: seriesDetail.seriesId, | ||||||
|  |                                         contentCount: seriesDetail.contentCount, | ||||||
|  |                                         contentList: seriesDetail.contentList | ||||||
|  |                                     ) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             .padding(.bottom, 10) | ||||||
|  |                             .background(Color.gray11) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             viewModel.seriesId = seriesId | ||||||
|  |             viewModel.getSeriesDetail() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     SeriesDetailView(seriesId: 0) | ||||||
|  | } | ||||||
| @@ -0,0 +1,151 @@ | |||||||
|  | // | ||||||
|  | //  SeriesDetailViewModel.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/29/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | final class SeriesDetailViewModel: ObservableObject { | ||||||
|  |     private let repository = SeriesRepository() | ||||||
|  |     private let userRepository = UserRepository() | ||||||
|  |     private var subscription = Set<AnyCancellable>() | ||||||
|  |      | ||||||
|  |     @Published var isLoading = false | ||||||
|  |     @Published var errorMessage = "" | ||||||
|  |     @Published var isShowPopup = false | ||||||
|  |      | ||||||
|  |     @Published var isFollow: Bool = false | ||||||
|  |     @Published var seriesDetail: GetSeriesDetailResponse? = nil | ||||||
|  |      | ||||||
|  |     var seriesId: Int = 0 | ||||||
|  |      | ||||||
|  |     func getSeriesDetail() { | ||||||
|  |         isLoading = true | ||||||
|  |          | ||||||
|  |         repository | ||||||
|  |             .getSeriesDetail(seriesId: seriesId) | ||||||
|  |             .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<GetSeriesDetailResponse>.self, from: responseData) | ||||||
|  |                      | ||||||
|  |                     if let data = decoded.data, decoded.success { | ||||||
|  |                         self.seriesDetail = data | ||||||
|  |                         self.isFollow = data.creator.isFollow | ||||||
|  |                     } 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 follow(_ creatorId: Int) { | ||||||
|  |         isLoading = true | ||||||
|  |          | ||||||
|  |         userRepository.creatorFollow(creatorId: creatorId) | ||||||
|  |             .sink { result in | ||||||
|  |                 switch result { | ||||||
|  |                 case .finished: | ||||||
|  |                     DEBUG_LOG("finish") | ||||||
|  |                 case .failure(let error): | ||||||
|  |                     ERROR_LOG(error.localizedDescription) | ||||||
|  |                 } | ||||||
|  |             } receiveValue: { [unowned self] response in | ||||||
|  |                 self.isLoading = false | ||||||
|  |                 let responseData = response.data | ||||||
|  |                  | ||||||
|  |                 do { | ||||||
|  |                     let jsonDecoder = JSONDecoder() | ||||||
|  |                     let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) | ||||||
|  |                      | ||||||
|  |                     if decoded.success { | ||||||
|  |                         self.isFollow = !self.isFollow | ||||||
|  |                     } 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 unFollow(_ creatorId: Int) { | ||||||
|  |         isLoading = true | ||||||
|  |          | ||||||
|  |         userRepository.creatorUnFollow(creatorId: creatorId) | ||||||
|  |             .sink { result in | ||||||
|  |                 switch result { | ||||||
|  |                 case .finished: | ||||||
|  |                     DEBUG_LOG("finish") | ||||||
|  |                 case .failure(let error): | ||||||
|  |                     ERROR_LOG(error.localizedDescription) | ||||||
|  |                 } | ||||||
|  |             } receiveValue: { [unowned self] response in | ||||||
|  |                 self.isLoading = false | ||||||
|  |                 let responseData = response.data | ||||||
|  |                  | ||||||
|  |                 do { | ||||||
|  |                     let jsonDecoder = JSONDecoder() | ||||||
|  |                     let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) | ||||||
|  |                      | ||||||
|  |                     if decoded.success { | ||||||
|  |                         self.isFollow = !self.isFollow | ||||||
|  |                     } 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) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     enum CurrentTab: String { | ||||||
|  |         case home, introduction | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     @Published var currentTab: CurrentTab = .home | ||||||
|  | } | ||||||
| @@ -10,6 +10,7 @@ import Moya | |||||||
|  |  | ||||||
| enum SeriesApi { | enum SeriesApi { | ||||||
|     case getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int) |     case getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int) | ||||||
|  |     case getSeriesDetail(seriesId: Int) | ||||||
| } | } | ||||||
|  |  | ||||||
| extension SeriesApi: TargetType { | extension SeriesApi: TargetType { | ||||||
| @@ -21,12 +22,15 @@ extension SeriesApi: TargetType { | |||||||
|         switch self { |         switch self { | ||||||
|         case .getSeriesList: |         case .getSeriesList: | ||||||
|             return "/audio-content/series" |             return "/audio-content/series" | ||||||
|  |              | ||||||
|  |         case .getSeriesDetail(let seriesId): | ||||||
|  |             return "/audio-content/series/\(seriesId)" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     var method: Moya.Method { |     var method: Moya.Method { | ||||||
|         switch self { |         switch self { | ||||||
|         case .getSeriesList: |         case .getSeriesList, .getSeriesDetail: | ||||||
|             return .get |             return .get | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -42,6 +46,9 @@ extension SeriesApi: TargetType { | |||||||
|             ] as [String : Any] |             ] as [String : Any] | ||||||
|              |              | ||||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) |             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||||
|  |              | ||||||
|  |         case .getSeriesDetail: | ||||||
|  |             return .requestPlain | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   | |||||||
| @@ -16,4 +16,8 @@ class SeriesRepository { | |||||||
|     func getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { |     func getSeriesList(creatorId: Int, sortType: SeriesListAllViewModel.SeriesSortType, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { | ||||||
|         return api.requestPublisher(.getSeriesList(creatorId: creatorId, sortType: sortType, page: page, size: size)) |         return api.requestPublisher(.getSeriesList(creatorId: creatorId, sortType: sortType, page: page, size: size)) | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     func getSeriesDetail(seriesId: Int) -> AnyPublisher<Response, MoyaError> { | ||||||
|  |         return api.requestPublisher(.getSeriesDetail(seriesId: seriesId)) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -181,6 +181,9 @@ struct ContentView: View { | |||||||
|             case .seriesAll(let creatorId): |             case .seriesAll(let creatorId): | ||||||
|                 SeriesListAllView(creatorId: creatorId) |                 SeriesListAllView(creatorId: creatorId) | ||||||
|                  |                  | ||||||
|  |             case .seriesDetail(let seriesId): | ||||||
|  |                 SeriesDetailView(seriesId: seriesId) | ||||||
|  |                  | ||||||
|             default: |             default: | ||||||
|                 EmptyView() |                 EmptyView() | ||||||
|                     .frame(width: 0, height: 0, alignment: .topLeading) |                     .frame(width: 0, height: 0, alignment: .topLeading) | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								SodaLive/Sources/UI/Component/SeriesDetailTabView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								SodaLive/Sources/UI/Component/SeriesDetailTabView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | // | ||||||
|  | //  SeriesDetailTabView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/30/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct SeriesDetailTabView: View { | ||||||
|  |      | ||||||
|  |     let title: String | ||||||
|  |     let width: CGFloat | ||||||
|  |     let isSelected: Bool | ||||||
|  |     let onClick: () -> Void | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(spacing: 0) { | ||||||
|  |             Text(title) | ||||||
|  |                 .font(.custom(isSelected ? Font.bold.rawValue : Font.medium.rawValue, size: 16.7)) | ||||||
|  |                 .foregroundColor(isSelected ? Color.button : Color.gray77) | ||||||
|  |                 .frame(width: width, height: 50) | ||||||
|  |              | ||||||
|  |             if isSelected { | ||||||
|  |                 Rectangle() | ||||||
|  |                     .foregroundColor(Color.button) | ||||||
|  |                     .frame(width: width, height: 3) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .contentShape(Rectangle()) | ||||||
|  |         .onTapGesture { onClick() } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     SeriesDetailTabView( | ||||||
|  |         title: "홈", | ||||||
|  |         width: 180, | ||||||
|  |         isSelected: true, | ||||||
|  |         onClick: {} | ||||||
|  |     ) | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								SodaLive/Sources/UI/Component/SeriesKeywordChipView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								SodaLive/Sources/UI/Component/SeriesKeywordChipView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | // | ||||||
|  | //  SeriesKeywordChipView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 4/29/24. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct SeriesKeywordChipView: View { | ||||||
|  |      | ||||||
|  |     let keyword: String | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         Text(keyword) | ||||||
|  |             .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |             .foregroundColor(Color.grayd2) | ||||||
|  |             .padding(.horizontal, 8) | ||||||
|  |             .padding(.vertical, 5.3) | ||||||
|  |             .background(Color.gray22) | ||||||
|  |             .cornerRadius(26.7) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     SeriesKeywordChipView(keyword: "#로맨스") | ||||||
|  | } | ||||||
| @@ -22,8 +22,10 @@ extension Color { | |||||||
|     static let gray55 = Color(hex: "555555") |     static let gray55 = Color(hex: "555555") | ||||||
|     static let gray77 = Color(hex: "777777") |     static let gray77 = Color(hex: "777777") | ||||||
|     static let gray90 = Color(hex: "909090") |     static let gray90 = Color(hex: "909090") | ||||||
|  |     static let gray97 = Color(hex: "979797") | ||||||
|     static let graybb = Color(hex: "bbbbbb") |     static let graybb = Color(hex: "bbbbbb") | ||||||
|     static let grayd2 = Color(hex: "d2d2d2") |     static let grayd2 = Color(hex: "d2d2d2") | ||||||
|  |     static let grayd8 = Color(hex: "d8d8d8") | ||||||
|     static let grayee = Color(hex: "eeeeee") |     static let grayee = Color(hex: "eeeeee") | ||||||
|      |      | ||||||
|     static let mainRed = Color(hex: "ff5c49") |     static let mainRed = Color(hex: "ff5c49") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung