From a45b91bbad41d43eb3ca977a872f10e2a7cc713a Mon Sep 17 00:00:00 2001 From: kimjw Date: Thu, 6 Aug 2026 22:34:07 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=EC=97=90=20?= =?UTF-8?q?=EC=88=AB=EC=9E=90=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA TC 1-34 - 한글/영문/숫자 혼합 입력 시 에러가 발생하던 문제 수정 NICKNAME_REGEX에 0-9가 빠져 있어 숫자가 포함된 닉네임이 거부됐습니다. 온보딩과 프로필 수정 화면에 동일한 정규식이 복사돼 있어 함께 수정했습니다. (온보딩만 고치면 가입은 되는데 수정은 거부되는 불일치가 생깁니다) Co-Authored-By: Claude Opus 5 (1M context) --- .../com/flint/presentation/onboarding/OnboardingUiState.kt | 4 ++-- .../presentation/setting/editprofile/EditProfileUiState.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/onboarding/OnboardingUiState.kt b/app/src/main/java/com/flint/presentation/onboarding/OnboardingUiState.kt index fd0d5a0c..e367b9e2 100644 --- a/app/src/main/java/com/flint/presentation/onboarding/OnboardingUiState.kt +++ b/app/src/main/java/com/flint/presentation/onboarding/OnboardingUiState.kt @@ -15,7 +15,7 @@ data class OnboardingTermsUiState( enum class NicknameErrorType { DUPLICATE, // 이미 사용 중인 닉네임 - INVALID_FORMAT // 한글, 영문 외 문자 포함 + INVALID_FORMAT // 한글, 영문, 숫자 외 문자 포함 } data class OnboardingProfileUiState( @@ -29,7 +29,7 @@ data class OnboardingProfileUiState( companion object { const val MAX_LENGTH = 8 const val MIN_LENGTH = 2 - private val NICKNAME_REGEX = Regex("^[가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z]+$") + private val NICKNAME_REGEX = Regex("^[가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9]+$") private val STANDALONE_KOREAN_REGEX = Regex("[ㄱ-ㅎㅏ-ㅣ]") fun isValidFormat(nickname: String): Boolean { diff --git a/app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt b/app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt index ea25a6f5..96428588 100644 --- a/app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt +++ b/app/src/main/java/com/flint/presentation/setting/editprofile/EditProfileUiState.kt @@ -15,7 +15,7 @@ data class EditProfileUiState( companion object { const val MAX_LENGTH = 8 const val MIN_LENGTH = 2 - private val NICKNAME_REGEX = Regex("^[가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z]+$") + private val NICKNAME_REGEX = Regex("^[가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9]+$") private val STANDALONE_KOREAN_REGEX = Regex("[ㄱ-ㅎㅏ-ㅣ]") fun isValidFormat(nickname: String): Boolean = From 75f25a2451561f8b6cc5b128fd65c21c9d839d6f Mon Sep 17 00:00:00 2001 From: kimjw Date: Thu, 6 Aug 2026 22:34:21 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=ED=99=88=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=9C=20=EC=BD=98=ED=85=90=EC=B8=A0=2010?= =?UTF-8?q?=EA=B0=9C=EA=B9=8C=EC=A7=80=EB=A7=8C=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA TC 3-23, 3-24 - 저장한 콘텐츠가 10개를 넘어도 계속 노출되던 문제 수정 getUserBookmarkedContents()는 프로필/저장한 콘텐츠 화면에서도 사용하므로 Repository가 아닌 HomeViewModel에서 잘랐습니다. totalCount는 프로필 쪽에서 쓰이므로 전체 개수를 유지합니다. Co-Authored-By: Claude Opus 5 (1M context) --- .../flint/presentation/home/HomeViewModel.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt b/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt index e2ffdf53..88868926 100644 --- a/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt @@ -13,6 +13,7 @@ import com.flint.domain.repository.UserRepository import com.flint.presentation.home.sideeffect.HomeSideEffect import com.flint.presentation.home.uistate.HomeUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -71,7 +72,18 @@ class HomeViewModel @Inject constructor( fun getBookmarkedContentList() = viewModelScope.launch { userRepository.getUserBookmarkedContents(userId = null) - .onSuccess { _bookmarkedContentListLoadState.emit(UiState.Success(it)) } + .onSuccess { bookmarkedContents -> + // 홈에서는 최근 저장한 콘텐츠 10개까지만 노출 (전체 목록은 프로필 > 저장한 콘텐츠에서 확인) + _bookmarkedContentListLoadState.emit( + UiState.Success( + bookmarkedContents.copy( + contents = bookmarkedContents.contents + .take(MAX_SAVED_CONTENT_COUNT) + .toImmutableList(), + ), + ), + ) + } .onFailure { Timber.e(it.message) } } @@ -86,4 +98,8 @@ class HomeViewModel @Inject constructor( .onSuccess { _homeSideEffect.emit(HomeSideEffect.ShowOttListBottomSheet(it)) } .onFailure { Timber.e(it.message) } } + + companion object { + private const val MAX_SAVED_CONTENT_COUNT = 10 + } } \ No newline at end of file From fa00e6248c78c5580f093a90097ef08f235e7f64 Mon Sep 17 00:00:00 2001 From: kimjw Date: Thu, 6 Aug 2026 22:34:32 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=20=EC=97=B0=EB=8F=99=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA TC 12-6, 12-7 - 계정 정보 영역에 이메일이 표시되지 않던 문제 대응 스웨거 MyProfileRes(GET /api/v1/users/me)에 email이 문서화돼 있으나 UserProfileResponseDto에 필드가 없어 값이 와도 받을 수 없는 상태였습니다. DTO/모델/매퍼/UiState까지 email을 연결하고 UI에 노출합니다. 단, 실기기 확인 결과 서버가 실제로는 email을 내려주지 않습니다 (null인 필드를 응답에서 생략하는 구조라 키 자체가 없음). 따라서 이 커밋만으로 TC 12-6/12-7이 닫히지는 않으며, 카카오 이메일 동의항목 수집 여부에 대한 백엔드 확인이 필요합니다. 이메일이 있으면 노출하고 없으면 로고만 표시하는 구조로 정리했습니다. 타 유저 응답(UserProfileRes)에는 email이 없고 DTO를 공유하므로 keywordRecalculatable과 동일하게 nullable + 기본값으로 처리했습니다. 긴 이메일이 "계정" 라벨을 밀어내지 않도록 trailingContent를 RowScope로 변경하고 weight(1f, fill = false)를 적용했습니다. Co-Authored-By: Claude Opus 5 (1M context) --- .../user/response/UserProfileResponseDto.kt | 3 +++ .../flint/domain/mapper/user/ProfileMapper.kt | 1 + .../model/user/UserProfileResponseModel.kt | 1 + .../presentation/setting/SettingScreen.kt | 21 +++++++++++++++++-- .../presentation/setting/SettingUiState.kt | 1 + .../presentation/setting/SettingViewModel.kt | 7 ++++++- 6 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt b/app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt index 03e13910..815b3130 100644 --- a/app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt +++ b/app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt @@ -13,6 +13,9 @@ data class UserProfileResponseDto( val isFliner: Boolean, @SerialName("nickname") val nickname: String, + // 내 프로필(/users/me) 응답에만 존재, 이메일 미보유 시 null + @SerialName("email") + val email: String? = null, @SerialName("keywordRecalculatable") val keywordRecalculatable: Boolean = false, ) diff --git a/app/src/main/java/com/flint/domain/mapper/user/ProfileMapper.kt b/app/src/main/java/com/flint/domain/mapper/user/ProfileMapper.kt index 257df653..97fe4974 100644 --- a/app/src/main/java/com/flint/domain/mapper/user/ProfileMapper.kt +++ b/app/src/main/java/com/flint/domain/mapper/user/ProfileMapper.kt @@ -9,5 +9,6 @@ fun UserProfileResponseDto.toModel(): UserProfileResponseModel = isFliner = isFliner, nickname = nickname, profileImageUrl = profileImageUrl, + email = email, keywordRecalculatable = keywordRecalculatable, ) \ No newline at end of file diff --git a/app/src/main/java/com/flint/domain/model/user/UserProfileResponseModel.kt b/app/src/main/java/com/flint/domain/model/user/UserProfileResponseModel.kt index f20eef88..3f2415e2 100644 --- a/app/src/main/java/com/flint/domain/model/user/UserProfileResponseModel.kt +++ b/app/src/main/java/com/flint/domain/model/user/UserProfileResponseModel.kt @@ -5,6 +5,7 @@ data class UserProfileResponseModel( val isFliner: Boolean, val nickname: String, val profileImageUrl: String?, + val email: String? = null, val keywordRecalculatable: Boolean = false, ) { companion object { diff --git a/app/src/main/java/com/flint/presentation/setting/SettingScreen.kt b/app/src/main/java/com/flint/presentation/setting/SettingScreen.kt index e02ee456..0b6e8c50 100644 --- a/app/src/main/java/com/flint/presentation/setting/SettingScreen.kt +++ b/app/src/main/java/com/flint/presentation/setting/SettingScreen.kt @@ -2,9 +2,11 @@ package com.flint.presentation.setting import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -27,6 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -111,7 +114,20 @@ private fun SettingScreen( SettingMenuItem( label = "계정", trailingContent = { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + uiState.email?.let { email -> + Text( + text = email, + style = FlintTheme.typography.body2R14, + color = FlintTheme.colors.gray100, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + } Image( painter = painterResource(R.drawable.ic_kakao_full), contentDescription = null, @@ -226,7 +242,7 @@ private fun SettingMenuItem( modifier: Modifier = Modifier, verticalPadding: Dp = 18.dp, onClick: () -> Unit = {}, - trailingContent: @Composable () -> Unit = {}, + trailingContent: @Composable RowScope.() -> Unit = {}, ) { Row( modifier = modifier @@ -253,6 +269,7 @@ private fun SettingScreenPreview() { uiState = SettingUiState( nickname = "한비두비세비", profileImageUrl = null, + email = "flint@kakao.com", ), onBackClick = {}, onEditProfileClick = {}, diff --git a/app/src/main/java/com/flint/presentation/setting/SettingUiState.kt b/app/src/main/java/com/flint/presentation/setting/SettingUiState.kt index ddf077fc..80f85366 100644 --- a/app/src/main/java/com/flint/presentation/setting/SettingUiState.kt +++ b/app/src/main/java/com/flint/presentation/setting/SettingUiState.kt @@ -3,5 +3,6 @@ package com.flint.presentation.setting data class SettingUiState( val nickname: String = "", val profileImageUrl: String? = null, + val email: String? = null, val isLogoutDialogVisible: Boolean = false, ) diff --git a/app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt b/app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt index 6f4bc27e..e3e9a74a 100644 --- a/app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt +++ b/app/src/main/java/com/flint/presentation/setting/SettingViewModel.kt @@ -40,7 +40,12 @@ class SettingViewModel @Inject constructor( viewModelScope.launch { userRepository.getUserProfile(userId = null) .onSuccess { profile -> - _uiState.update { it.copy(profileImageUrl = profile.profileImageUrl) } + _uiState.update { + it.copy( + profileImageUrl = profile.profileImageUrl, + email = profile.email, + ) + } } .onFailure { Timber.e(it) } } From 0d226007b9e853e32bd3098beb375d6981a84d1c Mon Sep 17 00:00:00 2001 From: kimjw Date: Thu, 6 Aug 2026 23:12:23 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20OTT=20=EB=AA=A9=EB=A1=9D=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=9D=98=20contentUrl=20=EA=B8=B0=EB=B3=B8=EA=B0=92?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9C=A0=EB=8B=9B=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버 스키마 GetOttResponse는 { ottId, name, logoUrl } 세 필드뿐이라 기본값 없는 contentUrl이 있으면 MissingFieldException으로 역직렬화가 실패합니다. 현재는 서버가 빈 배열을 주고 있어 드러나지 않지만, 실제 OTT 데이터가 채워지는 순간 홈 바텀시트가 깨집니다. ignoreUnknownKeys/coerceInputValues로는 막을 수 없어(누락 필드는 대상 아님) 기본값을 지정했습니다. 함께 추가한 유닛 테스트: - OttListResponseDtoTest: contentUrl 유무 양쪽 역직렬화 (4개) - NicknameValidationTest: 닉네임 숫자 허용 및 거부 규칙 (12개) 온보딩과 프로필 수정의 판정이 일치하는지도 함께 검증 Co-Authored-By: Claude Opus 5 (1M context) --- .../dto/ott/response/OttListResponseDto.kt | 3 +- .../data/dto/ott/OttListResponseDtoTest.kt | 87 +++++++++++++++ .../onboarding/NicknameValidationTest.kt | 101 ++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/com/flint/data/dto/ott/OttListResponseDtoTest.kt create mode 100644 app/src/test/java/com/flint/presentation/onboarding/NicknameValidationTest.kt diff --git a/app/src/main/java/com/flint/data/dto/ott/response/OttListResponseDto.kt b/app/src/main/java/com/flint/data/dto/ott/response/OttListResponseDto.kt index 0681ff49..50b20099 100644 --- a/app/src/main/java/com/flint/data/dto/ott/response/OttListResponseDto.kt +++ b/app/src/main/java/com/flint/data/dto/ott/response/OttListResponseDto.kt @@ -17,6 +17,7 @@ data class OttItemResponseDto( val name: String, @SerialName("logoUrl") val logoUrl: String, + // 서버 응답(GetOttResponse)에 없는 필드. 기본값이 없으면 역직렬화가 실패한다 @SerialName("contentUrl") - val contentUrl: String + val contentUrl: String = "", ) \ No newline at end of file diff --git a/app/src/test/java/com/flint/data/dto/ott/OttListResponseDtoTest.kt b/app/src/test/java/com/flint/data/dto/ott/OttListResponseDtoTest.kt new file mode 100644 index 00000000..d411f53f --- /dev/null +++ b/app/src/test/java/com/flint/data/dto/ott/OttListResponseDtoTest.kt @@ -0,0 +1,87 @@ +package com.flint.data.dto.ott + +import com.flint.data.dto.base.BaseResponse +import com.flint.data.dto.ott.response.OttListResponseDto +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * 콘텐츠별 OTT 목록 응답 역직렬화 테스트 + * + * QA TC 3-33, 3-34: 홈에서 콘텐츠 카드를 눌러도 바텀시트가 뜨지 않는 문제 + * + * GET /api/v1/contents/ott/{contentId} 의 서버 응답 스키마(GetOttResponse)는 + * { ottId, name, logoUrl } 세 필드뿐이고 contentUrl 은 존재하지 않는다. + * DTO 가 contentUrl 을 기본값 없는 필수 필드로 선언하면 역직렬화가 실패하고, + * 그 예외가 suspendRunCatching -> onFailure 로 흘러가 조용히 삼켜진다. + */ +class OttListResponseDtoTest { + + // NetworkModule 의 Json 설정과 동일하게 맞춘다 + private val json = Json { + ignoreUnknownKeys = true + coerceInputValues = true + explicitNulls = false + prettyPrint = true + } + + /** 스웨거 GetOttListRes 스키마 그대로 — contentUrl 없음 */ + private val serverResponse = """ + { + "status": 200, + "message": "OTT리스트 조회 성공", + "data": { + "otts": [ + { "ottId": "1", "name": "넷플릭스", "logoUrl": "https://cdn.flint/netflix.png" }, + { "ottId": "2", "name": "티빙", "logoUrl": "https://cdn.flint/tving.png" } + ] + } + } + """.trimIndent() + + @Test + fun `contentUrl 이 없는 서버 응답을 역직렬화할 수 있다`() { + val response = json.decodeFromString>(serverResponse) + + assertEquals(2, response.data.otts.size) + assertEquals("넷플릭스", response.data.otts[0].name) + assertEquals("https://cdn.flint/tving.png", response.data.otts[1].logoUrl) + } + + @Test + fun `contentUrl 이 없으면 빈 문자열로 채운다`() { + val response = json.decodeFromString>(serverResponse) + + assertEquals("", response.data.otts[0].contentUrl) + } + + @Test + fun `서버가 contentUrl 을 내려주면 그 값을 사용한다`() { + val withContentUrl = """ + { + "otts": [ + { + "ottId": "1", + "name": "넷플릭스", + "logoUrl": "https://cdn.flint/netflix.png", + "contentUrl": "https://netflix.com/title/123" + } + ] + } + """.trimIndent() + + val dto = json.decodeFromString(withContentUrl) + + assertEquals("https://netflix.com/title/123", dto.otts[0].contentUrl) + } + + @Test + fun `볼 수 있는 OTT 가 없으면 빈 목록으로 역직렬화된다`() { + val emptyResponse = """{ "otts": [] }""" + + val dto = json.decodeFromString(emptyResponse) + + assertEquals(0, dto.otts.size) + } +} diff --git a/app/src/test/java/com/flint/presentation/onboarding/NicknameValidationTest.kt b/app/src/test/java/com/flint/presentation/onboarding/NicknameValidationTest.kt new file mode 100644 index 00000000..22d345a0 --- /dev/null +++ b/app/src/test/java/com/flint/presentation/onboarding/NicknameValidationTest.kt @@ -0,0 +1,101 @@ +package com.flint.presentation.onboarding + +import com.flint.presentation.setting.editprofile.EditProfileUiState +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * 닉네임 유효성 검사 규칙 테스트 + * + * QA TC 1-34: 한글/영문/숫자 혼합 2~8자는 정상 입력 가능해야 한다 + * QA TC 1-40: 특수문자, 기호, 이모지, 한글/영문 외 다른 언어는 거부되어야 한다 + * + * 온보딩(OnboardingProfileUiState)과 프로필 수정(EditProfileUiState)에 + * 동일한 규칙이 각각 구현돼 있어 양쪽 모두 검증한다. + */ +class NicknameValidationTest { + + private fun bothAccept(nickname: String): Boolean { + val onboarding = OnboardingProfileUiState.isValidFormat(nickname) + val editProfile = EditProfileUiState.isValidFormat(nickname) + assertEquals( + "온보딩과 프로필 수정의 판정이 달라선 안 된다: \"$nickname\"", + onboarding, + editProfile, + ) + return onboarding + } + + // ---- TC 1-34: 숫자 허용 ---- + + @Test + fun `숫자만으로 이루어진 닉네임을 허용한다`() { + assertTrue(bothAccept("1234")) + } + + @Test + fun `한글과 숫자를 섞은 닉네임을 허용한다`() { + assertTrue(bothAccept("플린트2")) + } + + @Test + fun `영문과 숫자를 섞은 닉네임을 허용한다`() { + assertTrue(bothAccept("flint2")) + } + + @Test + fun `한글 영문 숫자를 모두 섞은 닉네임을 허용한다`() { + assertTrue(bothAccept("플린트flint7")) + } + + // ---- 기존 허용 범위 회귀 ---- + + @Test + fun `한글만으로 이루어진 닉네임을 허용한다`() { + assertTrue(bothAccept("플린트")) + } + + @Test + fun `영문만으로 이루어진 닉네임을 허용한다`() { + assertTrue(bothAccept("Flint")) + } + + @Test + fun `빈 문자열은 형식 오류로 보지 않는다`() { + // 입력 전 상태에서 에러 메시지가 뜨면 안 되므로 형식 검사는 통과시킨다 + assertTrue(bothAccept("")) + } + + // ---- TC 1-40: 거부 대상 ---- + + @Test + fun `특수문자가 포함되면 거부한다`() { + assertFalse(bothAccept("플린트!")) + assertFalse(bothAccept("flint_2")) + assertFalse(bothAccept("flint 2")) + } + + @Test + fun `이모지가 포함되면 거부한다`() { + assertFalse(bothAccept("플린트🔥")) + } + + @Test + fun `한글 영문 외 다른 언어는 거부한다`() { + assertFalse(bothAccept("플린트あ")) + assertFalse(bothAccept("플린트中")) + } + + // ---- 자음/모음 단독 입력 ---- + + @Test + fun `자음이나 모음 단독 입력을 감지한다`() { + assertTrue(OnboardingProfileUiState(nickname = "ㅋㅋㅋ").hasStandaloneKorean) + assertTrue(EditProfileUiState(nickname = "ㅋㅋㅋ").hasStandaloneKorean) + + assertFalse(OnboardingProfileUiState(nickname = "플린트2").hasStandaloneKorean) + assertFalse(EditProfileUiState(nickname = "플린트2").hasStandaloneKorean) + } +} From 71c29be1aa3f0629d073ed397832acd358080e8f Mon Sep 17 00:00:00 2001 From: kimjw Date: Thu, 6 Aug 2026 23:21:23 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=ED=99=88=20OTT=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=EB=A5=BC=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=98=20OTT=20=EC=A0=95=EB=B3=B4=EB=A1=9C?= =?UTF-8?q?=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA TC 3-33, 3-34 - 홈에서 저장 콘텐츠를 눌러도 바텀시트가 뜨지 않던 문제 원인은 서버가 GET /api/v1/contents/ott/{contentId} 에 대해 {"otts":[]} 빈 배열만 반환하는 것이었습니다. 빈 목록이면 HomeScreen 의 isNotEmpty() 가드에 걸려 아무 반응이 없었습니다. 반면 북마크 목록 응답은 같은 콘텐츠에 대해 getOttSimpleList 를 정상적으로 내려주고 있고, 프로필(ProfileScreen)과 저장한 콘텐츠(SavedContentScreen)는 이미 이 값을 사용합니다. 홈만 별도 API 를 호출하고 있어 홈에서만 실패했습니다. 홈도 동일하게 이미 로드된 북마크 목록의 getOttSimpleList 를 사용하도록 변경했습니다. 바텀시트는 OttType 의 로컬 iconRes/ottName 으로 렌더링하므로 서버의 logoUrl(현재 "adsf" 등 더미값)과 contentUrl 은 필요하지 않습니다. 부수 효과: - 콘텐츠 탭마다 발생하던 네트워크 호출이 사라짐 - OttShortCutListItem 의 OttType.valueOf() 는 runCatching 없이 호출되는데, 로컬 값은 이미 OttType 으로 검증된 값이라 IllegalArgumentException 위험이 없어짐 - 미사용이 된 ContentRepository 의존성 제거 Co-Authored-By: Claude Opus 5 (1M context) --- .../com/flint/presentation/home/HomeScreen.kt | 2 +- .../flint/presentation/home/HomeViewModel.kt | 24 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/home/HomeScreen.kt b/app/src/main/java/com/flint/presentation/home/HomeScreen.kt index 4394a743..2d7d97e3 100644 --- a/app/src/main/java/com/flint/presentation/home/HomeScreen.kt +++ b/app/src/main/java/com/flint/presentation/home/HomeScreen.kt @@ -85,7 +85,7 @@ fun HomeRoute( onFamousCollectionItemClick = { navigateToCollectionDetail(it) }, onFamousCollectionAllClick = { navigateToCollectionList(CollectionListRouteType.FAMOUS) }, onRecommendCollectionItemClick = { navigateToCollectionDetail(it) }, - onSavedContentItemClick = { viewModel.getOttListPerContent(it) }, + onSavedContentItemClick = { viewModel.showOttList(it) }, modifier = Modifier.padding(paddingValues), ) } diff --git a/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt b/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt index 88868926..cbd17da6 100644 --- a/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt @@ -7,7 +7,8 @@ import com.flint.core.common.util.UiState import com.flint.data.local.PreferencesManager import com.flint.domain.model.collection.CollectionListModel import com.flint.domain.model.content.BookmarkedContentListModel -import com.flint.domain.repository.ContentRepository +import com.flint.domain.model.ott.OttListModel +import com.flint.domain.model.ott.OttModel import com.flint.domain.repository.HomeRepository import com.flint.domain.repository.UserRepository import com.flint.presentation.home.sideeffect.HomeSideEffect @@ -30,7 +31,6 @@ class HomeViewModel @Inject constructor( private val preferencesManager: PreferencesManager, private val homeRepository: HomeRepository, private val userRepository: UserRepository, - private val contentRepository: ContentRepository, ) : ViewModel() { private val _userName = preferencesManager.getString(USER_NAME) @@ -93,10 +93,22 @@ class HomeViewModel @Inject constructor( .onFailure { Timber.e(it.message) } } - fun getOttListPerContent(contentId: String) = viewModelScope.launch { - contentRepository.getOttListPerContent(contentId) - .onSuccess { _homeSideEffect.emit(HomeSideEffect.ShowOttListBottomSheet(it)) } - .onFailure { Timber.e(it.message) } + // 콘텐츠별 OTT 목록 API(/contents/ott/{id})가 빈 배열만 반환하므로 + // 이미 로드된 북마크 목록의 OTT 정보를 사용한다. + // 프로필/저장한 콘텐츠 화면도 동일하게 getOttSimpleList를 쓴다. + fun showOttList(contentId: String) = viewModelScope.launch { + val otts = (_bookmarkedContentListLoadState.value as? UiState.Success) + ?.data + ?.contents + ?.find { it.id == contentId } + ?.getOttSimpleList + .orEmpty() + + _homeSideEffect.emit( + HomeSideEffect.ShowOttListBottomSheet( + OttListModel(otts = otts.map { OttModel(name = it.name) }), + ), + ) } companion object {