feat: 커뮤니티 글쓰기/수정

- 이미지 gif 등록 기능 추가
This commit is contained in:
2025-07-03 13:15:01 +09:00
parent 6ff0d8bd61
commit e4012a1301
6 changed files with 357 additions and 85 deletions

View File

@@ -1,5 +1,40 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>

View File

@@ -149,6 +149,7 @@ dependencies {
implementation "io.github.ParkSangGwon:tedpermission-normal:3.3.0"
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.github.yalantis:ucrop:2.2.10'
implementation 'com.github.zhpanvip:bannerviewpager:3.5.7'
implementation 'com.google.android.gms:play-services-oss-licenses:17.1.0'

View File

@@ -2,18 +2,24 @@ package kr.co.vividnext.sodalive.explorer.profile.creator_community.modify
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import coil.load
import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.github.dhaval2404.imagepicker.ImagePicker
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
import com.orhanobut.logger.Logger
import com.yalantis.ucrop.UCrop
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
@@ -26,6 +32,7 @@ import kr.co.vividnext.sodalive.databinding.ActivityCreatorCommunityModifyBindin
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
import org.koin.android.ext.android.inject
import java.io.File
class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModifyBinding>(
ActivityCreatorCommunityModifyBinding::inflate
@@ -34,8 +41,9 @@ class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModi
private val viewModel: CreatorCommunityModifyViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var croppedTempFile: File
private val imageResult = registerForActivityResult(
private val imageCropResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
@@ -45,25 +53,116 @@ class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModi
val fileUri = data?.data
if (fileUri != null) {
binding.ivContent.background = null
binding.ivContent.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8f.dpToPx()))
}
viewModel.imageUri = fileUri
handleCroppedImage(fileUri)
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
showWrongImageFile()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
showImagePickerError(data = data)
}
}
private val imageSelectResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
val mime = contentResolver.getType(fileUri)
if (mime.equals("image/gif", ignoreCase = true)) {
handleGifImage(fileUri)
} else {
launchCrop(fileUri)
}
} else {
showWrongImageFile()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
showImagePickerError(data = data)
}
}
private fun launchImagePicker() {
ImagePicker.with(this)
.galleryOnly()
.createIntent { imageSelectResult.launch(it) }
}
private fun launchCrop(fileUri: Uri) {
croppedTempFile = File.createTempFile("cropped_", ".jpg", cacheDir)
val destinationUri = Uri.fromFile(croppedTempFile)
val options = UCrop.Options().apply {
setFreeStyleCropEnabled(true)
setToolbarTitle("이미지 자르기")
}
val uCrop = UCrop.of(fileUri, destinationUri)
.withOptions(options)
.withMaxResultSize(1080, 1080)
imageCropResult.launch(uCrop.getIntent(this))
}
private fun handleGifImage(fileUri: Uri) {
binding.ivContent.background = null
Glide.with(this)
.asGif()
.load(fileUri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.imageUri = fileUri
}
private fun handleCroppedImage(fileUri: Uri) {
binding.ivContent.background = null
Glide.with(this)
.load(fileUri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.imageUri = fileUri
}
private fun showWrongImageFile() {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
private fun showImagePickerError(data: Intent?) {
Toast.makeText(
this,
ImagePicker.getError(data),
Toast.LENGTH_SHORT
).show()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
@@ -89,25 +188,32 @@ class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModi
)
}
override fun onDestroy() {
deleteCroppedTempFile()
super.onDestroy()
}
private fun deleteCroppedTempFile() {
try {
if (::croppedTempFile.isInitialized && croppedTempFile.exists()) {
val deleted = croppedTempFile.delete()
if (!deleted) {
Logger.w("임시 파일 삭제 실패: ${croppedTempFile.absolutePath}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "게시글 등록"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.ivPhotoPicker.setOnClickListener { launchImagePicker() }
if (SharedPreferenceManager.isAuth) {
binding.llSetAdult.visibility = View.VISIBLE
@@ -174,11 +280,17 @@ class CreatorCommunityModifyActivity : BaseActivity<ActivityCreatorCommunityModi
viewModel.imageUrlLiveData.observe(this) {
if (!it.isNullOrBlank()) {
binding.ivContent.background = null
binding.ivContent.loadUrl(it) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8f.dpToPx()))
}
Glide.with(this)
.load(it)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
}
}

View File

@@ -2,18 +2,23 @@ package kr.co.vividnext.sodalive.explorer.profile.creator_community.write
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import coil.load
import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.github.dhaval2404.imagepicker.ImagePicker
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
import com.orhanobut.logger.Logger
import com.yalantis.ucrop.UCrop
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
@@ -33,8 +38,9 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
private val viewModel: CreatorCommunityWriteViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var croppedTempFile: File
private val imageResult = registerForActivityResult(
private val imageCropResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
@@ -44,30 +50,120 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
val fileUri = data?.data
if (fileUri != null) {
binding.ivContent.background = null
binding.ivContent.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(8f.dpToPx()))
}
viewModel.setImageUri(fileUri)
binding.llRecordAudio.visibility = View.VISIBLE
handleCroppedImage(fileUri)
} else {
binding.llRecordAudio.visibility = View.GONE
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
showWrongImageFile()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
binding.llRecordAudio.visibility = View.GONE
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
showImagePickerError(data = data)
}
}
private val imageSelectResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
val mime = contentResolver.getType(fileUri)
if (mime.equals("image/gif", ignoreCase = true)) {
handleGifImage(fileUri)
} else {
launchCrop(fileUri)
}
} else {
showWrongImageFile()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
showImagePickerError(data = data)
}
}
private fun launchImagePicker() {
ImagePicker.with(this)
.galleryOnly()
.createIntent { imageSelectResult.launch(it) }
}
private fun launchCrop(fileUri: Uri) {
croppedTempFile = File.createTempFile("cropped_", ".jpg", cacheDir)
val destinationUri = Uri.fromFile(croppedTempFile)
val options = UCrop.Options().apply {
setFreeStyleCropEnabled(true)
setToolbarTitle("이미지 자르기")
}
val uCrop = UCrop.of(fileUri, destinationUri)
.withOptions(options)
.withMaxResultSize(1080, 1080)
imageCropResult.launch(uCrop.getIntent(this))
}
private fun handleGifImage(fileUri: Uri) {
binding.llRecordAudio.visibility = View.VISIBLE
binding.ivContent.background = null
Glide.with(this)
.asGif()
.load(fileUri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.setImageUri(fileUri)
}
private fun handleCroppedImage(fileUri: Uri) {
binding.llRecordAudio.visibility = View.VISIBLE
binding.ivContent.background = null
Glide.with(this)
.load(fileUri)
.placeholder(R.drawable.ic_place_holder)
.apply(
RequestOptions().transform(
RoundedCorners(
8f.dpToPx().toInt()
)
)
)
.into(binding.ivContent)
viewModel.setImageUri(fileUri)
}
private fun showWrongImageFile() {
binding.llRecordAudio.visibility = View.GONE
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
private fun showImagePickerError(data: Intent?) {
binding.llRecordAudio.visibility = View.GONE
Toast.makeText(
this,
ImagePicker.getError(data),
Toast.LENGTH_SHORT
).show()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
@@ -81,6 +177,9 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
override fun onDestroy() {
deleteAudioFile()
deleteCroppedTempFile()
super.onDestroy()
}
@@ -90,6 +189,19 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
}
}
private fun deleteCroppedTempFile() {
try {
if (::croppedTempFile.isInitialized && croppedTempFile.exists()) {
val deleted = croppedTempFile.delete()
if (!deleted) {
Logger.w("임시 파일 삭제 실패: ${croppedTempFile.absolutePath}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
@@ -102,19 +214,7 @@ class CreatorCommunityWriteActivity : BaseActivity<ActivityCreatorCommunityWrite
fragment.show(supportFragmentManager, fragment.tag)
}
binding.ivPhotoPicker.setOnClickListener {
ImagePicker.with(this)
.crop()
.galleryOnly()
.galleryMimeTypes( // Exclude gif images
mimeTypes = arrayOf(
"image/png",
"image/jpg",
"image/jpeg"
)
)
.createIntent { imageResult.launch(it) }
}
binding.ivPhotoPicker.setOnClickListener { launchImagePicker() }
if (SharedPreferenceManager.isAuth) {
binding.llSetAdult.visibility = View.VISIBLE

View File

@@ -41,20 +41,28 @@
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp" />
<RelativeLayout
android:layout_width="121.3dp"
android:layout_height="106.7dp"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp">
<ImageView
android:id="@+id/iv_content"
android:layout_width="106.7dp"
android:layout_height="106.7dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:background="@drawable/bg_round_corner_8_13181b"
android:contentDescription="@null"
android:maxWidth="300dp"
android:maxHeight="300dp"
android:minWidth="106.7dp"
android:minHeight="106.7dp"
android:padding="13.3dp"
android:src="@drawable/ic_logo2" />
android:src="@drawable/ic_logo2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_photo_picker"
@@ -62,11 +70,15 @@
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="-20dp"
android:background="@drawable/bg_round_corner_33_3_3bb9f1"
android:contentDescription="@null"
android:padding="10dp"
android:src="@drawable/ic_camera" />
</RelativeLayout>
android:src="@drawable/ic_camera"
app:layout_constraintBottom_toBottomOf="@+id/iv_content"
app:layout_constraintStart_toEndOf="@+id/iv_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<LinearLayout

View File

@@ -41,20 +41,28 @@
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp" />
<RelativeLayout
android:layout_width="121.3dp"
android:layout_height="106.7dp"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp">
<ImageView
android:id="@+id/iv_content"
android:layout_width="106.7dp"
android:layout_height="106.7dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:background="@drawable/bg_round_corner_8_13181b"
android:contentDescription="@null"
android:maxWidth="300dp"
android:maxHeight="300dp"
android:minWidth="106.7dp"
android:minHeight="106.7dp"
android:padding="13.3dp"
android:src="@drawable/ic_logo2" />
android:src="@drawable/ic_logo2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_photo_picker"
@@ -62,11 +70,15 @@
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="-20dp"
android:background="@drawable/bg_round_corner_33_3_3bb9f1"
android:contentDescription="@null"
android:padding="10dp"
android:src="@drawable/ic_camera" />
</RelativeLayout>
android:src="@drawable/ic_camera"
app:layout_constraintBottom_toBottomOf="@+id/iv_content"
app:layout_constraintStart_toEndOf="@+id/iv_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<LinearLayout
@@ -98,8 +110,8 @@
android:id="@+id/ll_record_audio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="24dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
@@ -129,8 +141,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_marginTop="13.3dp"
android:gravity="center_horizontal"
android:orientation="horizontal">
<TextView