투표기능 추가

This commit is contained in:
Yu Sung 2025-01-07 19:59:19 +09:00
parent a2b7fef39e
commit 7e13689763
7 changed files with 112 additions and 5 deletions

View File

@ -11,6 +11,7 @@ import Kingfisher
struct AuditionApplicantItemView: View { struct AuditionApplicantItemView: View {
let item: GetAuditionRoleApplicantItem let item: GetAuditionRoleApplicantItem
let onClickVote: (Int) -> Void
var body: some View { var body: some View {
VStack(spacing: 5.3) { VStack(spacing: 5.3) {
@ -47,6 +48,9 @@ struct AuditionApplicantItemView: View {
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color.gray77) .foregroundColor(Color.gray77)
} }
.onTapGesture {
onClickVote(item.applicantId)
}
} }
.padding(.vertical, 18.7) .padding(.vertical, 18.7)
@ -67,6 +71,7 @@ struct AuditionApplicantItemView: View {
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
voiceUrl: "", voiceUrl: "",
voteCount: 777 voteCount: 777
) ),
onClickVote: { _ in }
) )
} }

View File

@ -16,5 +16,5 @@ struct GetAuditionRoleApplicantItem: Decodable {
let nickname: String let nickname: String
let profileImageUrl: String let profileImageUrl: String
let voiceUrl: String let voiceUrl: String
let voteCount: Int var voteCount: Int
} }

View File

@ -14,6 +14,7 @@ enum AuditionApi {
case getAuditionRoleDetail(auditionRoleId: Int) case getAuditionRoleDetail(auditionRoleId: Int)
case getAuditionApplicantList(auditionRoleId: Int, sortType: AuditionApplicantSortType, page: Int, size: Int) case getAuditionApplicantList(auditionRoleId: Int, sortType: AuditionApplicantSortType, page: Int, size: Int)
case applyAudition(parameters: [MultipartFormData]) case applyAudition(parameters: [MultipartFormData])
case voteApplicant(applicantId: Int)
} }
extension AuditionApi: TargetType { extension AuditionApi: TargetType {
@ -37,6 +38,9 @@ extension AuditionApi: TargetType {
case .applyAudition: case .applyAudition:
return "/audition/applicant" return "/audition/applicant"
case .voteApplicant:
return "/audition/vote"
} }
} }
@ -46,7 +50,7 @@ extension AuditionApi: TargetType {
case .getAuditionList, .getAuditionDetail, . getAuditionRoleDetail, .getAuditionApplicantList: case .getAuditionList, .getAuditionDetail, . getAuditionRoleDetail, .getAuditionApplicantList:
return .get return .get
case .applyAudition: case .applyAudition, .voteApplicant:
return .post return .post
} }
} }
@ -77,6 +81,10 @@ extension AuditionApi: TargetType {
case .applyAudition(let parameters): case .applyAudition(let parameters):
return .uploadMultipart(parameters) return .uploadMultipart(parameters)
case .voteApplicant(let applicantId):
let request = VoteAuditionApplicantRequest(applicantId: applicantId)
return .requestJSONEncodable(request)
} }
} }

View File

@ -39,4 +39,8 @@ final class AuditionRepository {
func applyAudition(parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> { func applyAudition(parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.applyAudition(parameters: parameters)) return api.requestPublisher(.applyAudition(parameters: parameters))
} }
func voteApplicant(applicantId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.voteApplicant(applicantId: applicantId))
}
} }

View File

@ -87,8 +87,12 @@ struct AuditionRoleDetailView: View {
ForEach(0..<viewModel.applicantList.count, id: \.self) { ForEach(0..<viewModel.applicantList.count, id: \.self) {
let applicant = viewModel.applicantList[$0] let applicant = viewModel.applicantList[$0]
AuditionApplicantItemView(item: applicant) AuditionApplicantItemView(
.padding(.bottom, $0 == viewModel.applicantList.count - 1 ? 33 : 0) item: applicant,
onClickVote: {
viewModel.voteApplicant(applicantId: $0)
}
).padding(.bottom, $0 == viewModel.applicantList.count - 1 ? 33 : 0)
if $0 == viewModel.applicantList.count - 1 { if $0 == viewModel.applicantList.count - 1 {
Color.clear Color.clear
@ -193,6 +197,17 @@ struct AuditionRoleDetailView: View {
viewModel.deleteAllRecordingFilesWithNamePrefix("voiceon_now_voice") viewModel.deleteAllRecordingFilesWithNamePrefix("voiceon_now_voice")
} }
} }
if viewModel.isShowSodaDialog {
SodaDialog(
title: viewModel.dialogTitle,
desc: viewModel.dialogDesc,
confirmButtonTitle: "확인"
) {
viewModel.isShowSodaDialog = false
viewModel.isShowNotifyVote = false
}
}
} }
} }

View File

@ -36,6 +36,11 @@ final class AuditionRoleDetailViewModel: ObservableObject {
@Published var soundData: Data? = nil @Published var soundData: Data? = nil
@Published var phoneNumber = "" @Published var phoneNumber = ""
@Published var isShowNotifyVote = true
@Published var isShowSodaDialog = false
@Published var dialogTitle = ""
@Published var dialogDesc = ""
var page = 1 var page = 1
var isLast = false var isLast = false
private var pageSize = 10 private var pageSize = 10
@ -262,6 +267,62 @@ final class AuditionRoleDetailViewModel: ObservableObject {
} }
} }
func voteApplicant(applicantId: Int) {
isLoading = true
repository.voteApplicant(applicantId: applicantId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { 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 {
if self.isShowNotifyVote {
self.dialogTitle = "[오디션 응원]"
self.dialogDesc = "오디션을 응원하셨습니다\n(무료응원 : 1계정당 1일 1회)\n1캔으로 추가 응원을 해보세요."
self.isShowSodaDialog = true
}
if let index = self.applicantList.firstIndex(where: { $0.applicantId == applicantId }) {
var applicant = self.applicantList[index]
applicant.voteCount += 1
self.applicantList.remove(at: index)
self.applicantList.insert(applicant, at: index)
}
} else {
if let message = decoded.message {
if message.contains("오늘 응원은 여기까지") {
self.dialogTitle = "[오늘 응원 제한]"
self.dialogDesc = "오늘 응원은 여기까지!\n하루 최대 10회까지 이용이 가능합니다.\n내일 다시 이용해주세요."
self.isShowSodaDialog = true
} else {
self.errorMessage = message
self.isShowPopup = true
}
} else {
self.errorMessage = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
self.isShowPopup = true
}
}
} catch {
self.errorMessage = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
func deleteAllRecordingFilesWithNamePrefix(_ prefix: String) { func deleteAllRecordingFilesWithNamePrefix(_ prefix: String) {
let fileManager = FileManager.default let fileManager = FileManager.default
let documentsURL = getDocumentsDirectory() let documentsURL = getDocumentsDirectory()

View File

@ -0,0 +1,14 @@
//
// VoteAuditionApplicantRequest.swift
// SodaLive
//
// Created by klaus on 1/7/25.
//
import Foundation
struct VoteAuditionApplicantRequest: Encodable {
let applicantId: Int
let container: String = "ios"
let timezone: String = TimeZone.current.identifier
}