diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterApi.kt index 0f562400..80362205 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/CharacterApi.kt @@ -33,6 +33,16 @@ interface CharacterApi { @Query("size") size: Int ): Single> + // 내 배경 이미지 리스트 (프로필 + 무료 + 구매 이미지) + // getCharacterImageList와 파라미터/응답 동일, 엔드포인트만 다름 + @GET("/api/chat/character/image/my-list") + fun getMyCharacterImageList( + @Header("Authorization") authHeader: String, + @Query("characterId") characterId: Long, + @Query("page") page: Int, + @Query("size") size: Int + ): Single> + @POST("/api/chat/character/image/purchase") fun purchaseCharacterImage( @Header("Authorization") authHeader: String, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryRepository.kt index 46879ccf..465d5eb0 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryRepository.kt @@ -8,6 +8,9 @@ class CharacterGalleryRepository( fun getCharacterImageList(token: String, characterId: Long, page: Int, size: Int) = characterApi.getCharacterImageList(authHeader = token, characterId = characterId, page = page, size = size) + fun getMyCharacterImageList(token: String, characterId: Long, page: Int, size: Int) = + characterApi.getMyCharacterImageList(authHeader = token, characterId = characterId, page = page, size = size) + fun purchaseCharacterImage(token: String, imageId: Long) = characterApi.purchaseCharacterImage( authHeader = token, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatBackgroundPickerDialogFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatBackgroundPickerDialogFragment.kt new file mode 100644 index 00000000..f4130e4e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatBackgroundPickerDialogFragment.kt @@ -0,0 +1,227 @@ +package kr.co.vividnext.sodalive.chat.talk.room + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.edit +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.load +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.chat.character.detail.gallery.CharacterGalleryRepository +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.FragmentChatBackgroundPickerBinding +import kr.co.vividnext.sodalive.databinding.ItemChatBackgroundImageBinding +import org.koin.android.ext.android.inject + +/** + * 채팅방 배경 이미지 선택 다이얼로그 + * - 3열 Grid, 간격 0, 4:5 비율 + * - 캐릭터 프로필 + 무료 이미지 + 내가 구매한 이미지 (my-list) + * - 선택 항목은 1dp #3bb9f1 테두리 및 우하단 "현재 배경" 라벨 표시 + */ +class ChatBackgroundPickerDialogFragment : DialogFragment() { + + private var _binding: FragmentChatBackgroundPickerBinding? = null + private val binding get() = _binding!! + + private val repository: CharacterGalleryRepository by inject() + private val compositeDisposable = CompositeDisposable() + private lateinit var loadingDialog: LoadingDialog + + private var roomId: Long = 0L + private var characterId: Long = 0L + private var profileUrl: String = "" + + private val prefsName = "chat_room_prefs" + private fun bgUrlKey(roomId: Long) = "chat_bg_url_room_$roomId" + private fun bgImageIdKey(roomId: Long) = "chat_bg_image_id_room_$roomId" + + private lateinit var adapter: BgAdapter + private val items = mutableListOf() + private var selectedUrl: String? = null + private var selectedId: Long? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, android.R.style.Theme_Black_NoTitleBar_Fullscreen) + roomId = arguments?.getLong(ARG_ROOM_ID) ?: 0L + // Activity에서 characterId/profileUrl 조회 (공개 getter 사용 예정) + val act = activity as? ChatRoomActivity + characterId = act?.getCharacterId() ?: 0L + profileUrl = act?.getCharacterProfileUrl().orEmpty() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentChatBackgroundPickerBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) + setupUi() + loadData() + } + + private fun setupUi() { + binding.ivClose.setOnClickListener { dismiss() } + binding.tvTitle.text = "배경 사진 선택" + + binding.rvGrid.layoutManager = GridLayoutManager(requireContext(), 3) + adapter = BgAdapter { item -> + onSelect(item) + } + binding.rvGrid.adapter = adapter + } + + private fun loadData() { + // 초기 선택: 저장된 값 사용 (URL + ID 병행) + val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE) + selectedUrl = prefs.getString(bgUrlKey(roomId), null) + val savedId = prefs.getLong(bgImageIdKey(roomId), -1L) + selectedId = if (savedId > 0) savedId else null + + items.clear() + + if (characterId > 0) { + val token = "Bearer ${SharedPreferenceManager.token}" + loadingDialog.show(resources.displayMetrics.widthPixels) + val d = repository.getMyCharacterImageList(token, characterId, page = 0, size = 60) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ resp -> + val list = resp.data?.items.orEmpty() + items.addAll(list.map { BgItem(id = it.id, url = it.imageUrl) }) + adapter.submit(items, selectedId, selectedUrl) + + // 마이그레이션: 과거에 URL만 저장되어 있고 ID가 비어 있는 경우 + if (selectedId == null && !selectedUrl.isNullOrBlank()) { + val found = items.firstOrNull { it.url == selectedUrl } + if (found != null) { + selectedId = found.id + // UI 갱신 + adapter.updateSelected(found) + // 영구 저장(ID 동기화) + val p = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE) + p.edit { + putLong(bgImageIdKey(roomId), found.id) + } + } + } + + loadingDialog.dismiss() + }, { _ -> + // 실패 시에도 현재까지의 목록 표시(없으면 빈 목록) + adapter.submit(items, selectedId, selectedUrl) + loadingDialog.dismiss() + }) + compositeDisposable.add(d) + } else { + // characterId가 없으면 서버 요청 불가: 현재 저장된 선택 상태만 반영 + adapter.submit(items, selectedId, selectedUrl) + } + } + + private fun onSelect(item: BgItem) { + selectedUrl = item.url + selectedId = item.id.takeIf { it > 0 } + saveAndApply(item) + adapter.updateSelected(item) + } + + private fun saveAndApply(item: BgItem) { + val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE) + prefs.edit { + putString(bgUrlKey(roomId), item.url) + if (item.id > 0) putLong(bgImageIdKey(roomId), item.id) else remove(bgImageIdKey(roomId)) + } + (activity as? ChatRoomActivity)?.setChatBackground(item.url) + } + + override fun onDestroyView() { + super.onDestroyView() + try { + if (this::loadingDialog.isInitialized) loadingDialog.dismiss() + } catch (_: Throwable) { } + compositeDisposable.clear() + _binding = null + } + + data class BgItem(val id: Long, val url: String) + + private class BgAdapter( + private val onClick: (BgItem) -> Unit + ) : RecyclerView.Adapter() { + private val data = mutableListOf() + private var selectedUrl: String? = null + private var selectedId: Long? = null + + @SuppressLint("NotifyDataSetChanged") + fun submit(items: List, selectedId: Long?, selectedUrl: String?) { + data.clear() + data.addAll(items) + this.selectedId = selectedId + this.selectedUrl = selectedUrl + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun updateSelected(item: BgItem) { + this.selectedId = item.id.takeIf { it > 0 } + this.selectedUrl = item.url + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BgVH { + val binding = ItemChatBackgroundImageBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return BgVH(binding, onClick) + } + + override fun getItemCount(): Int = data.size + + override fun onBindViewHolder(holder: BgVH, position: Int) { + holder.bind(data[position], selectedId, selectedUrl) + } + } + + private class BgVH( + private val binding: ItemChatBackgroundImageBinding, + private val onClick: (BgItem) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: BgItem, selectedId: Long?, selectedUrl: String?) { + binding.ivImage.load(item.url) { + placeholder(R.drawable.ic_placeholder_profile) + error(R.drawable.ic_placeholder_profile) + } + val selected = (selectedId != null && item.id == selectedId) || + (selectedUrl != null && selectedUrl == item.url) + binding.tvCurrent.visibility = if (selected) View.VISIBLE else View.GONE + binding.viewBorder.visibility = if (selected) View.VISIBLE else View.GONE + binding.root.setOnClickListener { onClick(item) } + } + } + + companion object { + private const val ARG_ROOM_ID = "arg_room_id" + fun newInstance(roomId: Long): ChatBackgroundPickerDialogFragment { + val f = ChatBackgroundPickerDialogFragment() + f.arguments = Bundle().apply { putLong(ARG_ROOM_ID, roomId) } + return f + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt index 024e9c79..11129589 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt @@ -119,11 +119,8 @@ class ChatRoomActivity : BaseActivity( // 프로필 이미지 (공용 유틸 + 둥근 모서리 적용) loadProfileImage(binding.ivProfile, info.profileImageUrl) - // 배경 프로필 이미지 (5.5) - binding.ivBackgroundProfile.load(info.profileImageUrl) { - placeholder(R.drawable.ic_placeholder_profile) - error(R.drawable.ic_placeholder_profile) - } + // 배경 이미지: 저장된 값 우선, 없으면 프로필로 저장/적용 + applyBackgroundFromPrefsOrProfile(info.profileImageUrl) // 타입 배지 텍스트 및 배경 val (badgeText, badgeBg) = when (info.characterType) { @@ -840,6 +837,37 @@ class ChatRoomActivity : BaseActivity( binding.viewCharacterDim.isVisible = visible } + private fun bgUrlPrefKey(): String = "chat_bg_url_room_$roomId" + + fun setChatBackground(url: String) { + binding.ivBackgroundProfile.load(url) { + placeholder(R.drawable.ic_placeholder_profile) + error(R.drawable.ic_placeholder_profile) + } + } + + private fun applyBackgroundFromPrefsOrProfile(profileUrl: String) { + val key = bgUrlPrefKey() + val saved = prefs.getString(key, null) + val target = when { + !saved.isNullOrBlank() -> saved + profileUrl.isNotBlank() -> { + prefs.edit { putString(key, profileUrl) } + profileUrl + } + else -> null + } + if (!target.isNullOrBlank()) { + setChatBackground(target) + } else { + binding.ivBackgroundProfile.setImageResource(R.drawable.ic_placeholder_profile) + } + } + + fun getCharacterId(): Long = characterInfo?.characterId ?: 0L + + fun getCharacterProfileUrl(): String = characterInfo?.profileImageUrl ?: "" + fun onResetChatRequested() { val title = "대화 초기화" val desc = "지금까지의 대화가 모두 초기화 되고 새롭게 대화를 시작합니다." diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomMoreDialogFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomMoreDialogFragment.kt index 9b0d2612..42ddef7c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomMoreDialogFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomMoreDialogFragment.kt @@ -57,18 +57,11 @@ class ChatRoomMoreDialogFragment : DialogFragment() { switch?.toggle() } - // 배경 사진 변경 (임시 안내) + // 배경 사진 변경: 배경 선택 다이얼로그 표시 view.findViewById(R.id.row_bg_change)?.setOnClickListener { - // TODO: 배경 선택 다이얼로그 연결 (기본 프로필 + 구매한 캐릭터 이미지) - SodaDialog( - requireActivity(), - layoutInflater, - title = getString(R.string.app_name), - desc = "배경 사진 변경은 곧 제공됩니다.", - confirmButtonTitle = "확인", - confirmButtonClick = {}, - cancelButtonTitle = "" - ).show(resources.displayMetrics.widthPixels) + val roomIdArg = arguments?.getLong(ARG_ROOM_ID) ?: 0L + ChatBackgroundPickerDialogFragment.newInstance(roomIdArg) + .show(parentFragmentManager, "ChatBackgroundPicker") } // 대화 초기화: Activity에 위임 diff --git a/app/src/main/res/drawable/bg_chat_bg_selected_border.xml b/app/src/main/res/drawable/bg_chat_bg_selected_border.xml new file mode 100644 index 00000000..2160117a --- /dev/null +++ b/app/src/main/res/drawable/bg_chat_bg_selected_border.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_chat_background_picker.xml b/app/src/main/res/layout/fragment_chat_background_picker.xml new file mode 100644 index 00000000..3ebf04c5 --- /dev/null +++ b/app/src/main/res/layout/fragment_chat_background_picker.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_chat_room_more_dialog.xml b/app/src/main/res/layout/fragment_chat_room_more_dialog.xml index 15d6aa4f..6f49fc92 100644 --- a/app/src/main/res/layout/fragment_chat_room_more_dialog.xml +++ b/app/src/main/res/layout/fragment_chat_room_more_dialog.xml @@ -1,7 +1,6 @@ @@ -14,8 +13,7 @@ android:background="@android:color/transparent" android:gravity="center_vertical" android:orientation="horizontal" - android:paddingStart="16dp" - android:paddingEnd="16dp" + android:paddingHorizontal="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -25,32 +23,32 @@ android:layout_width="24dp" android:layout_height="24dp" android:contentDescription="@string/a11y_back" - android:src="@drawable/ic_back"/> + android:src="@drawable/ic_back" /> + app:layout_constraintStart_toEndOf="@id/iv_close" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + android:paddingHorizontal="24dp"> + android:textColor="#B0BEC5" + android:textSize="18sp" /> + android:paddingHorizontal="24dp"> + android:textColor="#B0BEC5" + android:textSize="18sp" /> + android:paddingHorizontal="24dp" + android:paddingVertical="12dp"> + android:textColor="#B0BEC5" + android:textSize="18sp" /> + android:textSize="16sp" /> + android:paddingHorizontal="24dp"> + android:textColor="#B0BEC5" + android:textSize="18sp" /> diff --git a/app/src/main/res/layout/item_chat_background_image.xml b/app/src/main/res/layout/item_chat_background_image.xml new file mode 100644 index 00000000..a3958c96 --- /dev/null +++ b/app/src/main/res/layout/item_chat_background_image.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + +