diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt index 7a0601e..88a2be0 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt @@ -7,11 +7,14 @@ import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel import kr.co.vividnext.sodalive.audition.role.GetAuditionRoleDetailResponse import kr.co.vividnext.sodalive.audition.vote.VoteAuditionApplicantRequest import kr.co.vividnext.sodalive.common.ApiResponse -import kr.co.vividnext.sodalive.live.room.menu.UpdateLiveMenuRequest +import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.Path import retrofit2.http.Query @@ -44,6 +47,14 @@ interface AuditionApi { @Header("Authorization") authHeader: String ): Single> + @POST("/audition/applicant") + @Multipart + fun applyAudition( + @Part contentFile: MultipartBody.Part, + @Part("request") request: RequestBody, + @Header("Authorization") authHeader: String + ): Single> + @POST("/audition/vote") fun voteApplicant( @Body request: VoteAuditionApplicantRequest, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt index 3422ec9..7b810b1 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt @@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.audition import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel import kr.co.vividnext.sodalive.audition.vote.VoteAuditionApplicantRequest +import okhttp3.MultipartBody +import okhttp3.RequestBody class AuditionRepository( private val api: AuditionApi @@ -53,4 +55,14 @@ class AuditionRepository( request = request, authHeader = token ) + + fun applyAudition( + contentFile: MultipartBody.Part, + request: RequestBody, + token: String + ) = api.applyAudition( + contentFile = contentFile, + request = request, + authHeader = token + ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/ApplicationMethodDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/ApplicationMethodDialog.kt new file mode 100644 index 0000000..007908b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/ApplicationMethodDialog.kt @@ -0,0 +1,55 @@ +package kr.co.vividnext.sodalive.audition.applicant + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import kr.co.vividnext.sodalive.databinding.DialogApplicationMethodBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class ApplicationMethodDialog( + activity: Activity, + layoutInflater: LayoutInflater, + onClickFileUpload: () -> Unit, + onClickRecord: () -> Unit +) { + private val alertDialog: AlertDialog + + val dialogView = DialogApplicationMethodBinding.inflate(layoutInflater) + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + dialogView.ivClose.setOnClickListener { + alertDialog.dismiss() + } + + dialogView.llUploadFile.setOnClickListener { + alertDialog.dismiss() + onClickFileUpload() + } + + dialogView.llRecord.setOnClickListener { + alertDialog.dismiss() + onClickRecord() + } + } + + fun show(width: Int) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/ApplyAuditionRoleRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/ApplyAuditionRoleRequest.kt new file mode 100644 index 0000000..8bd4c4e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/ApplyAuditionRoleRequest.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.audition.applicant + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class ApplyAuditionRoleRequest( + @SerializedName("roleId") val roleId: Long, + @SerializedName("phoneNumber") val phoneNumber: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplyDialogFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplyDialogFragment.kt new file mode 100644 index 0000000..5adf8e5 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplyDialogFragment.kt @@ -0,0 +1,83 @@ +package kr.co.vividnext.sodalive.audition.applicant + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.Toast +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kr.co.vividnext.sodalive.databinding.FragmentAuditionApplyDialogBinding + +class AuditionApplyDialogFragment( + private val fileName: String, + private val onClickApply: (String) -> Unit, + private val onClickClose: () -> Unit +) : BottomSheetDialogFragment() { + private lateinit var binding: FragmentAuditionApplyDialogBinding + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + + dialog.setOnShowListener { + val d = it as BottomSheetDialog + val bottomSheet = d.findViewById( + com.google.android.material.R.id.design_bottom_sheet + ) + if (bottomSheet != null) { + val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheetBehavior.isDraggable = false + } + } + + return dialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentAuditionApplyDialogBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.ivClose.setOnClickListener { + onClickClose() + dismiss() + } + + binding.tvAuditionApply.setOnClickListener { + val phoneNumber = binding.etPhoneNumber.text.toString() + if (phoneNumber.isBlank() || phoneNumber.length != 11) { + Toast.makeText( + activity, + "잘못된 연락처 입니다.\n다시 입력해 주세요.", + Toast.LENGTH_LONG + ).show() + } else if (!binding.tvAgree.isSelected) { + Toast.makeText( + activity, + "연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다.", + Toast.LENGTH_LONG + ).show() + } else { + dismiss() + onClickApply(binding.etPhoneNumber.text.toString()) + } + } + + binding.tvAgree.setOnClickListener { + it.isSelected = !it.isSelected + } + + binding.tvAudioFileName.text = fileName + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt index c346791..ce7e614 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt @@ -4,25 +4,33 @@ import android.content.Intent import android.graphics.Rect import android.net.Uri import android.os.Bundle +import android.provider.OpenableColumns import android.view.View import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.RoundedCornersTransformation import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audition.applicant.ApplicationMethodDialog import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantListAdapter +import kr.co.vividnext.sodalive.audition.applicant.AuditionApplyDialogFragment import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.base.SodaDialog import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.RealPathUtil import kr.co.vividnext.sodalive.databinding.ActivityAuditionRoleDetailBinding +import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.RecordingVoiceFragment import kr.co.vividnext.sodalive.extensions.dpToPx import org.koin.android.ext.android.inject +import java.io.File class AuditionRoleDetailActivity : BaseActivity( ActivityAuditionRoleDetailBinding::inflate -) { +), RecordingVoiceFragment.OnAudioRecordedListener { private val viewModel: AuditionRoleDetailViewModel by inject() private lateinit var loadingDialog: LoadingDialog @@ -30,6 +38,38 @@ class AuditionRoleDetailActivity : BaseActivity + val resultCode = result.resultCode + val data = result.data + + if (resultCode == RESULT_OK) { + val fileUri = data?.data + if (fileUri != null) { + viewModel.audioFileName = getFileName(fileUri).toString() + viewModel.audioFile = File( + RealPathUtil.getRealPath(applicationContext, fileUri!!) + ) + showAuditionApplyDialog() + } else { + Toast.makeText( + this, + "잘못된 녹음 파일 입니다.\n다시 선택해 주세요.", + Toast.LENGTH_SHORT + ).show() + } + } else { + Toast.makeText( + this, + "잘못된 녹음 파일 입니다.\n다시 선택해 주세요.", + Toast.LENGTH_SHORT + ).show() + } + } override fun onCreate(savedInstanceState: Bundle?) { auditionRoleId = intent.getLongExtra(Constants.EXTRA_AUDITION_ROLE_ID, 0) @@ -76,6 +116,21 @@ class AuditionRoleDetailActivity : BaseActivity + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex != -1 && cursor.moveToFirst()) { + fileName = cursor.getString(nameIndex) + } + } + } else if (scheme == "file") { + val file = File(uri.path ?: "") + fileName = file.name + } + + return fileName + } + + override fun onAudioRecorded(file: File) { + deleteAudioFile() + viewModel.audioFile = file + viewModel.audioFileName = file.name + showAuditionApplyDialog() + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailViewModel.kt index 46dac28..458e770 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailViewModel.kt @@ -2,16 +2,25 @@ package kr.co.vividnext.sodalive.audition.role import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.google.gson.Gson import com.google.gson.annotations.SerializedName import com.orhanobut.logger.Logger import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers import kr.co.vividnext.sodalive.audition.AuditionRepository +import kr.co.vividnext.sodalive.audition.applicant.ApplyAuditionRoleRequest import kr.co.vividnext.sodalive.audition.applicant.GetAuditionRoleApplicantItem import kr.co.vividnext.sodalive.audition.vote.VoteAuditionApplicantRequest import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okio.BufferedSink +import java.io.File import java.util.TimeZone class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : BaseViewModel() { @@ -41,6 +50,8 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : var page = 1 var isLast = false + var audioFile: File? = null + var audioFileName: String = "" private var auditionRoleId = -1L private val pageSize = 10 @@ -171,6 +182,78 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : ) } + fun applyAudition(auditionRoleId: Long, phoneNumber: String, onSuccess: () -> Unit) { + if (audioFile == null) { + _toastLiveData.value = "잘못된 녹음 파일 입니다.\n다시 선택해 주세요." + return + } + + _isLoading.value = true + + val request = ApplyAuditionRoleRequest(roleId = auditionRoleId, phoneNumber = phoneNumber) + val requestJson = Gson().toJson(request) + + val contentFile = MultipartBody.Part.createFormData( + "contentFile", + audioFile!!.name, + body = object : RequestBody() { + override fun contentType(): MediaType { + return "audio/*".toMediaType() + } + + override fun writeTo(sink: BufferedSink) { + audioFile!!.inputStream().use { inputStream -> + val buffer = ByteArray(512) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + sink.write(buffer, 0, bytesRead) + } + } + } + + override fun contentLength(): Long { + return audioFile!!.length() + } + } + ) + + compositeDisposable.add( + repository.applyAudition( + contentFile = contentFile, + request = requestJson.toRequestBody("text/plain".toMediaType()), + token = "Bearer ${SharedPreferenceManager.token}" + ).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success) { + _applicantListLiveData.value = emptyList() + page = 1 + isLast = false + getAuditionRoleDetail(auditionRoleId = auditionRoleId) + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + ) + ) + } + fun setSortType(sortType: AuditionApplicantSortType) { val prevSortType = _sortTypeLiveData.value!! diff --git a/app/src/main/res/drawable-xxhdpi/ic_mic_color_button.png b/app/src/main/res/drawable-xxhdpi/ic_mic_color_button.png new file mode 100644 index 0000000..08c059c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_mic_color_button.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_note_square.png b/app/src/main/res/drawable-xxhdpi/ic_note_square.png new file mode 100644 index 0000000..2fba913 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_note_square.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_upload.png b/app/src/main/res/drawable-xxhdpi/ic_upload.png new file mode 100644 index 0000000..45db730 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_upload.png differ diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_13181b.xml b/app/src/main/res/drawable/bg_round_corner_5_3_13181b.xml new file mode 100644 index 0000000..2ea3cec --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_13181b.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/layout/dialog_application_method.xml b/app/src/main/res/layout/dialog_application_method.xml new file mode 100644 index 0000000..5213024 --- /dev/null +++ b/app/src/main/res/layout/dialog_application_method.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_audition_apply_dialog.xml b/app/src/main/res/layout/fragment_audition_apply_dialog.xml new file mode 100644 index 0000000..a0f3021 --- /dev/null +++ b/app/src/main/res/layout/fragment_audition_apply_dialog.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +