Compare commits

..

5 Commits

Author SHA1 Message Date
Yu Sung 8f62c17971 크리에이터 콘텐츠 리스트 - 고정 콘텐츠 핀 추가 2024-01-29 15:28:03 +09:00
Yu Sung 12d2c09434 콘텐츠 상세 - 콘텐츠 고정/해제 기능 추가 2024-01-29 15:22:45 +09:00
Yu Sung bd818918f3 콘텐츠 상세
- 미리듣기 없는 콘텐츠는 재생 버튼이 보이지 않도록 수정
2024-01-26 13:36:46 +09:00
Yu Sung aa87f0367b 인기 콘텐츠 카테고리 뱃지 - stroke 색상 변경 2024-01-26 12:59:08 +09:00
Yu Sung 34f2348aa0 콘텐츠 업로드
- 미리듣기 여부 선택 버튼 추가
2024-01-26 10:57:57 +09:00
22 changed files with 406 additions and 57 deletions

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_pin.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_pin_cancel.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_trash_can.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@ -34,6 +34,8 @@ enum ContentApi {
case getAudioContentListByCurationId(curationId: Int, page: Int, size: Int, sort: ContentCurationViewModel.Sort)
case getContentRanking(page: Int, size: Int, sortType: String)
case getContentRankingSortType
case pinContent(contentId: Int)
case unpinContent(contentId: Int)
}
extension ContentApi: TargetType {
@ -117,6 +119,12 @@ extension ContentApi: TargetType {
case .getContentRankingSortType:
return "/audio-content/ranking-sort-type"
case .pinContent(let contentId):
return "/audio-content/pin-to-the-top/\(contentId)"
case .unpinContent(let contentId):
return "/audio-content/unpin-at-the-top/\(contentId)"
}
}
@ -131,10 +139,10 @@ extension ContentApi: TargetType {
case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList:
return .get
case .likeContent, .modifyAudioContent, .modifyComment:
case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent:
return .put
case .registerComment, .orderAudioContent, .addAllPlaybackTracking, .uploadAudioContent, .donation:
case .registerComment, .orderAudioContent, .addAllPlaybackTracking, .uploadAudioContent, .donation, .pinContent:
return .post
case .deleteAudioContent:
@ -259,6 +267,9 @@ extension ContentApi: TargetType {
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .pinContent, .unpinContent:
return .requestPlain
}
}

View File

@ -48,6 +48,12 @@ struct ContentListItemView: View {
.padding(2.6)
.background(Color(hex: "222222"))
.cornerRadius(2.6)
if item.isPin {
Image("ic_pin")
.resizable()
.frame(width: 13.3, height: 13.3)
}
}
Text(item.title)
@ -121,6 +127,7 @@ struct ContentListItemView_Previews: PreviewProvider {
duration: "00:04:43",
likeCount: 2,
commentCount: 0,
isPin: true,
isAdult: false,
isScheduledToOpen: true
)

View File

@ -112,4 +112,12 @@ final class ContentRepository {
func getContentRanking(page: Int, size: Int, sortType: String = "매출") -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getContentRanking(page: page, size: size, sortType: sortType))
}
func pinContent(contentId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.pinContent(contentId: contentId))
}
func unpinContent(contentId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.unpinContent(contentId: contentId))
}
}

View File

@ -288,62 +288,84 @@ struct ContentCreateView: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.top, 26.7)
}
if !viewModel.isFree {
VStack(spacing: 10) {
Text("미리듣기 시간 설정")
VStack(spacing: 13.3) {
Text("미리듣기")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(maxWidth: .infinity, alignment: .leading)
Text("미리듣기 시간을 직접 설정하지 않으면 콘텐츠 앞부분 30초가 자동으로 설정됩니다. 미리듣기의 시간제한은 없습니다.")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
VStack(spacing: 5.3) {
Text("시작 시간")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.frame(maxWidth: .infinity, alignment: .leading)
TextField("00:00:00", text: $viewModel.previewStartTime)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.6))
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 16.7)
.padding(.horizontal, 13.3)
.background(Color(hex: "222222"))
.cornerRadius(6.7)
.keyboardType(.default)
.multilineTextAlignment(.center)
SelectButtonView(title: "생성", isChecked: viewModel.isGeneratePreview) {
if !viewModel.isGeneratePreview {
viewModel.isGeneratePreview = true
}
}
VStack(spacing: 5.3) {
Text("종료 시간")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.frame(maxWidth: .infinity, alignment: .leading)
TextField("00:00:30", text: $viewModel.previewEndTime)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.6))
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 16.7)
.padding(.horizontal, 13.3)
.background(Color(hex: "222222"))
.cornerRadius(6.7)
.keyboardType(.default)
.multilineTextAlignment(.center)
SelectButtonView(title: "생성 안 함", isChecked: !viewModel.isGeneratePreview) {
if viewModel.isGeneratePreview {
viewModel.isGeneratePreview = false
}
}
}
.padding(.top, 3.3)
}
.padding(.top, 26.7)
if viewModel.isGeneratePreview {
VStack(spacing: 10) {
Text("미리듣기 시간 설정")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(maxWidth: .infinity, alignment: .leading)
Text("미리듣기 시간을 직접 설정하지 않으면 콘텐츠 앞부분 30초가 자동으로 설정됩니다. 미리듣기의 시간제한은 없습니다.")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
VStack(spacing: 5.3) {
Text("시작 시간")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.frame(maxWidth: .infinity, alignment: .leading)
TextField("00:00:00", text: $viewModel.previewStartTime)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.6))
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 16.7)
.padding(.horizontal, 13.3)
.background(Color(hex: "222222"))
.cornerRadius(6.7)
.keyboardType(.default)
.multilineTextAlignment(.center)
}
VStack(spacing: 5.3) {
Text("종료 시간")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "d2d2d2"))
.frame(maxWidth: .infinity, alignment: .leading)
TextField("00:00:30", text: $viewModel.previewEndTime)
.autocapitalization(.none)
.disableAutocorrection(true)
.font(.custom(Font.bold.rawValue, size: 14.6))
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 16.7)
.padding(.horizontal, 13.3)
.background(Color(hex: "222222"))
.cornerRadius(6.7)
.keyboardType(.default)
.multilineTextAlignment(.center)
}
}
.padding(.top, 3.3)
}
.padding(.top, 26.7)
}
}
}
.padding(.top, 26.7)

View File

@ -56,11 +56,13 @@ final class ContentCreateViewModel: ObservableObject {
if isFree {
priceString = "0"
isOnlyRental = false
isGeneratePreview = true
}
}
}
@Published var isOnlyRental = false
@Published var isGeneratePreview = true
@Published var previewStartTime: String = ""
@Published var previewEndTime: String = ""
@ -95,9 +97,10 @@ final class ContentCreateViewModel: ObservableObject {
themeId: theme!.id,
isAdult: isAdult,
isOnlyRental: isOnlyRental,
isGeneratePreview: isGeneratePreview,
isCommentAvailable: isAvailableComment,
previewStartTime: previewStartTime.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 ? previewStartTime : nil,
previewEndTime: previewEndTime.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 ? previewEndTime : nil
previewStartTime: isGeneratePreview && previewStartTime.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 ? previewStartTime : nil,
previewEndTime: isGeneratePreview && previewEndTime.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 ? previewEndTime : nil
)
var multipartData = [MultipartFormData]()

View File

@ -17,6 +17,7 @@ struct CreateAudioContentRequest: Encodable {
let themeId: Int
let isAdult: Bool
let isOnlyRental: Bool
let isGeneratePreview: Bool
let isCommentAvailable: Bool
let previewStartTime: String?
let previewEndTime: String?

View File

@ -11,7 +11,10 @@ struct ContentDetailMenuView: View {
@Binding var isShowing: Bool
let isPin: Bool
let isShowCreatorMenu: Bool
let pinAction: () -> Void
let modifyAction: () -> Void
let deleteAction: () -> Void
let reportAction: () -> Void
@ -28,7 +31,26 @@ struct ContentDetailMenuView: View {
VStack(spacing: 13.3) {
if isShowCreatorMenu {
HStack(spacing: 0) {
HStack(spacing: 13.3) {
Image(isPin ? "ic_pin_cancel" : "ic_pin")
Text(isPin ? "내 채널에 고정 취소" : "내 채널에 고정")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(.white)
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 26.7)
.contentShape(Rectangle())
.onTapGesture {
isShowing = false
pinAction()
}
HStack(spacing: 13.3) {
Image("ic_make_message")
Text("수정")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(.white)
@ -43,7 +65,9 @@ struct ContentDetailMenuView: View {
modifyAction()
}
HStack(spacing: 0) {
HStack(spacing: 13.3) {
Image("ic_trash_can")
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(.white)

View File

@ -35,7 +35,7 @@ struct ContentDetailPlayView: View {
)
.cornerRadius(10.7, corners: [.topLeft, .topRight])
if audioContent.releaseDate == nil || audioContent.creator.creatorId == UserDefaults.int(forKey: .userId) {
if audioContent.releaseDate == nil && !isAlertPreview || (isAlertPreview && audioContent.isActivePreview) {
Image(isPlaying() ? "btn_audio_content_pause" : isAlertPreview ? "btn_audio_content_preview_play" : "btn_audio_content_play")
.onTapGesture {
if isPlaying() {

View File

@ -219,7 +219,19 @@ struct ContentDetailView: View {
VStack(spacing: 0) {
ContentDetailMenuView(
isShowing: $viewModel.isShowReportMenu,
isPin: viewModel.audioContent!.isPin,
isShowCreatorMenu: viewModel.audioContent!.creator.creatorId == UserDefaults.int(forKey: .userId),
pinAction: {
if viewModel.audioContent!.isPin {
viewModel.unpinContent(contentId: contentId)
} else {
if viewModel.audioContent!.isAvailablePin {
viewModel.pinContent(contentId: contentId)
} else {
viewModel.isShowNoticePinContentPopup = true
}
}
},
modifyAction: {
if viewModel.audioContent!.creator.creatorId == UserDefaults.int(forKey: .userId) {
AppState
@ -284,6 +296,21 @@ struct ContentDetailView: View {
viewModel.donation(can: can, comment: comment)
}
}
if viewModel.isShowNoticePinContentPopup {
SodaDialog(
title: "고정 한도 도달",
desc: "이 콘텐츠를 고정하시겠어요? " +
"채널에 콘텐츠를 최대 3개까지 고정할 수 있습니다." +
"이 콘텐츠를 고정하면 가장 오래된 콘텐츠가 대체됩니다.",
confirmButtonTitle: "확인",
confirmButtonAction: {
viewModel.pinContent(contentId: contentId)
},
cancelButtonTitle: "취소",
cancelButtonAction: {}
)
}
}
}
.sheet(

View File

@ -34,6 +34,7 @@ final class ContentDetailViewModel: ObservableObject {
@Published var isShowReportMenu = false
@Published var isShowReportView = false
@Published var isShowDeleteConfirm = false
@Published var isShowNoticePinContentPopup = false
var contentId: Int = 0 {
didSet {
@ -448,4 +449,84 @@ final class ContentDetailViewModel: ObservableObject {
.store(in: &subscription)
}
}
func pinContent(contentId: Int) {
isLoading = true
repository.pinContent(contentId: contentId)
.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.errorMessage = "고정되었습니다"
self.isShowPopup = true
self.getAudioContentDetail()
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
func unpinContent(contentId: Int) {
isLoading = true
repository.unpinContent(contentId: contentId)
.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.errorMessage = "해제되었습니다"
self.isShowPopup = true
self.getAudioContentDetail()
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
}

View File

@ -18,6 +18,7 @@ struct GetAudioContentDetailResponse: Decodable {
let price: Int
let duration: String
let releaseDate: String?
let isActivePreview: Bool
let isAdult: Bool
let isMosaic: Bool
let isOnlyRental: Bool
@ -31,6 +32,8 @@ struct GetAudioContentDetailResponse: Decodable {
let likeCount: Int
let commentList: [GetAudioContentCommentListItem]
let commentCount: Int
let isPin: Bool
let isAvailablePin: Bool
let creator: AudioContentCreator
}

View File

@ -20,18 +20,18 @@ struct ContentMainRankingSortView: View {
let sort = sorts[index]
Text(sort)
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color(hex: selectedSort == sort ? "9970ff" : "777777"))
.foregroundColor(selectedSort == sort ? Color.button : Color.gray77)
.padding(.horizontal, 13.3)
.padding(.vertical, 9.3)
.border(
Color(hex: selectedSort == sort ? "9970ff" : "eeeeee"),
selectedSort == sort ? Color.button : Color.grayee,
width: 0.5
)
.cornerRadius(16.7)
.overlay(
RoundedRectangle(cornerRadius: CGFloat(16.7))
.stroke(lineWidth: 0.5)
.foregroundColor(Color(hex: selectedSort == sort ? "9970ff" : "eeeeee"))
.foregroundColor(selectedSort == sort ? Color.button : Color.grayee)
)
.onTapGesture {
if selectedSort != sort {

View File

@ -67,7 +67,7 @@ struct CreatorCommunityAllView: View {
ZStack {
if viewModel.isShowReportMenu {
VStack(spacing: 0) {
ContentDetailMenuView(
CreatorCommunityMenuView(
isShowing: $viewModel.isShowReportMenu,
isShowCreatorMenu: creatorId == UserDefaults.int(forKey: .userId),
modifyAction: {

View File

@ -0,0 +1,97 @@
//
// CreatorCommunityMenuView.swift
// SodaLive
//
// Created by klaus on 1/29/24.
//
import SwiftUI
struct CreatorCommunityMenuView: View {
@Binding var isShowing: Bool
let isShowCreatorMenu: Bool
let modifyAction: () -> Void
let deleteAction: () -> Void
let reportAction: () -> Void
var body: some View {
ZStack {
Color.black
.opacity(0.7)
.ignoresSafeArea()
.onTapGesture { isShowing = false }
VStack(spacing: 0) {
Spacer()
VStack(spacing: 13.3) {
if isShowCreatorMenu {
HStack(spacing: 13.3) {
Image("ic_make_message")
Text("수정")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(.white)
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 26.7)
.contentShape(Rectangle())
.onTapGesture {
isShowing = false
modifyAction()
}
HStack(spacing: 13.3) {
Image("ic_trash_can")
Text("삭제")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(.white)
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 26.7)
.contentShape(Rectangle())
.onTapGesture {
isShowing = false
deleteAction()
}
} else {
HStack(spacing: 0) {
Text("신고")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(.white)
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 26.7)
.contentShape(Rectangle())
.onTapGesture {
isShowing = false
reportAction()
}
}
}
.padding(24)
.background(Color(hex: "222222"))
.cornerRadius(13.3, corners: [.topLeft, .topRight])
}
}
}
}
#Preview {
CreatorCommunityMenuView(
isShowing: .constant(true),
isShowCreatorMenu: true,
modifyAction: {},
deleteAction: {},
reportAction: {}
)
}

View File

@ -77,6 +77,7 @@ struct GetAudioContentListItem: Decodable {
let duration: String?
let likeCount: Int
let commentCount: Int
let isPin: Bool
let isAdult: Bool
let isScheduledToOpen: Bool
}

View File

@ -74,6 +74,7 @@ struct UserProfileContentView_Previews: PreviewProvider {
duration: "00:04:43",
likeCount: 2,
commentCount: 0,
isPin: true,
isAdult: false,
isScheduledToOpen: false
)