fix(chat): 배경 선택 다이얼로그에서 초기 선택 복원이 되지 않는 문제 수정
- 선택 상태를 URL 비교에서 이미지 ID 우선 방식으로 변경
- URL만 저장된 기존 데이터에 대해 목록 로드 후 URL→ID 마이그레이션 추가
- SharedPreferences에 chat_bg_image_id_room_{roomId} 키 도입(호환 위해 URL 키 유지)
This commit is contained in:
@@ -42,7 +42,8 @@ interface TalkApi {
|
||||
@GET("/api/chat/room/{roomId}/enter")
|
||||
fun enterChatRoom(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("roomId") roomId: Long
|
||||
@Path("roomId") roomId: Long,
|
||||
@Query("characterImageId") characterImageId: Long?
|
||||
): Single<ApiResponse<ChatRoomEnterResponse>>
|
||||
|
||||
// 메시지 전송 API
|
||||
|
||||
@@ -42,12 +42,10 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
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<BgItem>()
|
||||
private var selectedUrl: String? = null
|
||||
private var selectedId: Long? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -88,12 +86,10 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
// 초기 선택: 저장된 값 사용 (URL + ID 병행)
|
||||
// 초기 선택: 저장된 이미지 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) {
|
||||
@@ -105,38 +101,23 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
.subscribe({ resp ->
|
||||
val list = resp.data?.items.orEmpty()
|
||||
items.addAll(list.map { BgItem(id = it.id, url = it.imageUrl) })
|
||||
adapter.submit(items, selectedId, selectedUrl)
|
||||
adapter.submit(items, selectedId)
|
||||
|
||||
// 마이그레이션: 과거에 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)
|
||||
adapter.submit(items, selectedId)
|
||||
loadingDialog.dismiss()
|
||||
})
|
||||
compositeDisposable.add(d)
|
||||
} else {
|
||||
// characterId가 없으면 서버 요청 불가: 현재 저장된 선택 상태만 반영
|
||||
adapter.submit(items, selectedId, selectedUrl)
|
||||
adapter.submit(items, selectedId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSelect(item: BgItem) {
|
||||
selectedUrl = item.url
|
||||
selectedId = item.id.takeIf { it > 0 }
|
||||
saveAndApply(item)
|
||||
adapter.updateSelected(item)
|
||||
@@ -145,8 +126,10 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
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))
|
||||
if (item.id > 0) putLong(
|
||||
bgImageIdKey(roomId),
|
||||
item.id
|
||||
) else remove(bgImageIdKey(roomId))
|
||||
}
|
||||
(activity as? ChatRoomActivity)?.setChatBackground(item.url)
|
||||
}
|
||||
@@ -155,7 +138,8 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
super.onDestroyView()
|
||||
try {
|
||||
if (this::loadingDialog.isInitialized) loadingDialog.dismiss()
|
||||
} catch (_: Throwable) { }
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
compositeDisposable.clear()
|
||||
_binding = null
|
||||
}
|
||||
@@ -166,22 +150,19 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
private val onClick: (BgItem) -> Unit
|
||||
) : RecyclerView.Adapter<BgVH>() {
|
||||
private val data = mutableListOf<BgItem>()
|
||||
private var selectedUrl: String? = null
|
||||
private var selectedId: Long? = null
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun submit(items: List<BgItem>, selectedId: Long?, selectedUrl: String?) {
|
||||
fun submit(items: List<BgItem>, selectedId: Long?) {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -195,7 +176,7 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onBindViewHolder(holder: BgVH, position: Int) {
|
||||
holder.bind(data[position], selectedId, selectedUrl)
|
||||
holder.bind(data[position], selectedId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,13 +184,15 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
|
||||
private val binding: ItemChatBackgroundImageBinding,
|
||||
private val onClick: (BgItem) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: BgItem, selectedId: Long?, selectedUrl: String?) {
|
||||
fun bind(item: BgItem, selectedId: Long?) {
|
||||
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)
|
||||
val selected = when {
|
||||
selectedId != null -> item.id == selectedId
|
||||
else -> item.id == 0L // 저장된 ID가 없으면 프로필(id=0)을 선택으로 간주
|
||||
}
|
||||
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) }
|
||||
|
||||
@@ -86,8 +86,8 @@ class ChatRepository(
|
||||
* 통합 채팅방 입장: 서버 캐릭터 정보 + 최신 메시지 수신 후 로컬 DB 업데이트
|
||||
* - 로컬 데이터가 없더라도 서버 응답을 기준으로 동기화
|
||||
*/
|
||||
fun enterChatRoom(token: String, roomId: Long): Single<ChatRoomEnterResponse> {
|
||||
return talkApi.enterChatRoom(authHeader = token, roomId = roomId)
|
||||
fun enterChatRoom(token: String, roomId: Long, characterImageId: Long?): Single<ChatRoomEnterResponse> {
|
||||
return talkApi.enterChatRoom(authHeader = token, roomId = roomId, characterImageId = characterImageId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { response ->
|
||||
val data = ensureSuccess(response)
|
||||
|
||||
@@ -96,8 +96,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
binding.tvName.text = ""
|
||||
binding.ivProfile.setImageResource(R.drawable.ic_placeholder_profile)
|
||||
binding.ivBackgroundProfile.setImageResource(R.drawable.ic_placeholder_profile)
|
||||
// 저장된 배경이 있으면 즉시 적용 (네트워크 응답 전 초기 진입에서도 반영)
|
||||
applyBackgroundFromPrefsOrProfile("")
|
||||
// 배경은 서버 응답 수신 시 적용 (기본은 플레이스홀더 유지)
|
||||
// 배지는 기본 Clone으로 둔다가 실제 값으로 갱신 (디자인 기본 배경도 clone)
|
||||
binding.tvCharacterTypeBadge.text = "Clone"
|
||||
binding.tvCharacterTypeBadge.setBackgroundResource(R.drawable.bg_character_status_clone)
|
||||
@@ -121,8 +120,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
|
||||
// 프로필 이미지 (공용 유틸 + 둥근 모서리 적용)
|
||||
loadProfileImage(binding.ivProfile, info.profileImageUrl)
|
||||
// 배경 이미지: 저장된 값 우선, 없으면 프로필로 저장/적용
|
||||
applyBackgroundFromPrefsOrProfile(info.profileImageUrl)
|
||||
// 배경 이미지는 서버 응답의 backgroundImageUrl을 우선 적용 (enter 완료 시 처리)
|
||||
|
||||
// 타입 배지 텍스트 및 배경
|
||||
val (badgeText, badgeBg) = when (info.characterType) {
|
||||
@@ -607,12 +605,21 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
|
||||
// 2) 서버 통합 API로 동기화 및 UI 갱신
|
||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||
val networkDisposable = chatRepository.enterChatRoom(token = token, roomId = roomId)
|
||||
val bgImageId = getSavedBackgroundImageId()
|
||||
val networkDisposable = chatRepository.enterChatRoom(token = token, roomId = roomId, characterImageId = bgImageId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ response ->
|
||||
// 캐릭터 정보 바인딩
|
||||
setCharacterInfo(response.character)
|
||||
|
||||
// 배경 이미지 적용: 서버 응답의 backgroundImageUrl 우선, 없으면 프로필 사용
|
||||
val bgUrl = response.backgroundImageUrl ?: response.character.profileImageUrl
|
||||
if (bgUrl.isNotBlank()) {
|
||||
setChatBackground(bgUrl)
|
||||
} else {
|
||||
binding.ivBackgroundProfile.setImageResource(R.drawable.ic_placeholder_profile)
|
||||
}
|
||||
|
||||
// 메시지 정렬(오래된 -> 최신) + 동시간대는 messageId 오름차순으로 안정화
|
||||
val sorted =
|
||||
response.messages.sortedWith(compareBy<ServerChatMessage> { it.createdAt }.thenBy { it.messageId })
|
||||
@@ -870,6 +877,13 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||
|
||||
fun getCharacterProfileUrl(): String = characterInfo?.profileImageUrl ?: ""
|
||||
|
||||
private fun bgImageIdPrefKey(): String = "chat_bg_image_id_room_$roomId"
|
||||
|
||||
private fun getSavedBackgroundImageId(): Long? {
|
||||
val id = prefs.getLong(bgImageIdPrefKey(), -1L)
|
||||
return if (id > 0) id else null
|
||||
}
|
||||
|
||||
fun onResetChatRequested() {
|
||||
val title = "대화 초기화"
|
||||
val desc = "지금까지의 대화가 모두 초기화 되고 새롭게 대화를 시작합니다."
|
||||
|
||||
@@ -13,5 +13,6 @@ data class ChatRoomEnterResponse(
|
||||
@SerializedName("messages") val messages: List<ServerChatMessage>,
|
||||
@SerializedName("hasMoreMessages") val hasMoreMessages: Boolean,
|
||||
@SerializedName("totalRemaining") val totalRemaining: Int = 0,
|
||||
@SerializedName("nextRechargeAtEpoch") val nextRechargeAtEpoch: Long? = null
|
||||
@SerializedName("nextRechargeAtEpoch") val nextRechargeAtEpoch: Long? = null,
|
||||
@SerializedName("bgImageUrl") val backgroundImageUrl: String? = null
|
||||
)
|
||||
|
||||
@@ -27,10 +27,10 @@ class ChatRepositoryTest {
|
||||
val character = CharacterInfo(10, "name", "", CharacterType.CLONE)
|
||||
val resp = ChatRoomEnterResponse(99, character, serverMessages, hasMoreMessages = false)
|
||||
|
||||
every { api.enterChatRoom(any(), any()) } returns Single.just(ApiResponse(true, resp, null))
|
||||
every { api.enterChatRoom(any(), any(), any()) } returns Single.just(ApiResponse(true, resp, null))
|
||||
coEvery { dao.getNthLatestCreatedAt(any(), any()) } returns null
|
||||
|
||||
val result = repo.enterChatRoom("token", 99).blockingGet()
|
||||
val result = repo.enterChatRoom("token", 99, null).blockingGet()
|
||||
|
||||
// 반환 검증
|
||||
assertEquals(99, result.roomId)
|
||||
|
||||
Reference in New Issue
Block a user