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