뱃지 상세 화면 구현 및 배지 실시간 알림 SSE 연동#147
Hidden character warning
Conversation
* **feat: 뱃지 기능 모듈(`feature:badge`) 추가 및 연동**
* 뱃지 목록 및 상세 조회를 위한 `feature:badge:api`와 `feature:badge:impl` 모듈을 신규 생성했습니다.
* `BadgeScreen`, `BadgeViewModel` 및 MVI 기반의 `BadgeUiState`, `BadgeUiIntent`, `BadgeUiEffect`를 구현했습니다.
* `BadgeNavKey`를 통한 네비게이션 연동 및 `MyScreen`에서 뱃지 화면으로의 이동 로직을 추가했습니다.
* **feat: 서버 데이터 기반 뱃지 도메인 모델 및 API 연동**
* 기존의 하드코딩된 `BadgeType` Enum을 제거하고 서버 데이터를 반영하는 `Badge`, `BadgeDetail` 모델을 추가했습니다.
* `BadgeService`, `BadgeRemoteDataSource`, `BadgeRepository`를 구현하여 서버로부터 뱃지 정보를 조회하도록 개선했습니다.
* `FetchBadgesUseCase`, `FetchBadgeDetailUseCase` 등 뱃지 관련 유즈케이스를 정의했습니다.
* **feat: SSE(Server-Sent Events)를 활용한 뱃지 획득 실시간 알림**
* `api/stream/badges` 엔드포인트를 통해 실시간 뱃지 이벤트를 수신하는 SSE 연동 로직을 `BadgeRemoteDataSource`에 구현했습니다.
* `MainActivity` 및 `PrezelApp`에서 뱃지 이벤트 스트림을 구독하고, 새 뱃지 획득 시 스낵바를 통해 사용자에게 알림을 표시하는 기능을 추가했습니다.
* **refactor: `PrezelBadge` 컴포넌트 및 UI 로직 개선**
* `PrezelBadge`가 로컬 리소스 대신 이미지 URL(`PrezelAsyncImage`)을 사용하도록 변경했습니다.
* `MyScreen` 및 `BadgeSection`에서 정적 뱃지 데이터를 서버에서 받아온 데이터 기반으로 렌더링하도록 수정했습니다.
* 불필요해진 로컬 뱃지 드로어블 리소스(`badge_start.xml`) 및 관련 문자열 리소스를 제거했습니다.
* **build: 프로젝트 의존성 및 설정 업데이트**
* 새 모듈 추가에 따라 `settings.gradle.kts` 및 각 모듈의 `build.gradle.kts` 의존성 설정을 업데이트했습니다.
* **refactor: Ktor SSE 플러그인 도입 및 스트림 로직 개선**
* `HttpClientFactory`에 Ktor `SSE` 플러그인 설정을 추가했습니다.
* `BadgeRemoteDataSourceImpl`에서 수동으로 응답 채널을 읽던 로직을 `serverSentEvents` 확장 함수를 사용하는 방식으로 리팩터링했습니다.
* SSE 이벤트 파싱 로직을 `BadgeSseEventParser`로 분리하고, `badge_unlocked` 이벤트에 대한 처리를 구체화했습니다.
* **feat: 뱃지 관련 데이터 모델 필드 추가 및 타입 수정**
* `BadgeEvent` 및 `BadgeEventResponse` 모델에 `introduction`, `imageUrl` 필드를 추가했습니다.
* `GetBadgeResponse`와 `GetBadgeDetailResponse`의 `unlockedAt` 타입을 `String?`로 변경하여 null 허용 상태를 반영했습니다.
* `BadgeUiState`에서 `selectedBadge`를 조회할 때 `firstOrNull`을 사용하도록 하여 안정성을 높였습니다.
* **refactor: 인증 상태에 따른 뱃지 이벤트 구독 제어**
* `PrezelAppState`에 현재 경로가 인증이 필요한 페이지인지 확인하는 `isAuthenticated` 로직을 추가했습니다.
* `ObserveBadgeEvents` 컴포저블에서 인증된 사용자만 뱃지 이벤트 스트림을 구독하도록 수정했습니다.
* **fix: 의존성 주입 및 매핑 로직 수정**
* `BadgeViewModel`에 누락되었던 `@HiltViewModel` 어노테이션을 추가했습니다.
* `BadgeRepositoryImpl`에서 `unlockedAt` 매핑 시 불필요한 `isNotBlank` 체크 로직을 제거했습니다.
* **feat: 배지 알림 스낵바 위치 조정 로직 추가**
* `ObserveBadgeEvents`에서 하단 내비게이션 바 표시 여부(`shouldShowNavigationBar`)를 파악하도록 수정했습니다.
* 스낵바 호출 시 `useRaisedPosition` 파라미터에 해당 상태를 전달하여, 내비게이션 바와 스낵바가 겹치지 않도록 위치를 조정했습니다.
* `LaunchedEffect`의 키 값에서 불필요한 UseCase 의존성을 제거하고 `isAuthenticated` 변화에만 반응하도록 최적화했습니다.
* **refactor: `BadgeRemoteDataSourceImpl` 내 상수 가시성 제한**
* 외부에서 참조되지 않는 `BADGE_SSE_SOCKET_TIMEOUT_MILLIS`, `BADGE_SSE_RETRY_DELAY_MILLIS`, `badgeStreamUrl` 상수를 `private`으로 변경했습니다.
* **style: 배지 알림 액션 문구 수정**
* 스낵바의 확인 버튼 텍스트를 `보기`에서 `보러가기`로 변경하여 사용자 행동 유도를 강화했습니다.
# Conflicts: # Prezel/app/build.gradle.kts
* **feat: 뱃지 상세 모달 및 관련 UI 컴포넌트 추가**
* 뱃지 이미지의 잠금/에러 상태를 처리하는 `BadgeDetailImage` 컴포넌트를 추가했습니다.
* 뱃지 획득 조건 및 설명을 표시하는 `BadgeDetailInfo` 컴포넌트를 추가했습니다.
* 상세 정보를 화면 전체에 표시하는 `BadgeDetailModal`을 구현하고, `BadgeScreen`에 연동했습니다.
* **feat: 뱃지 상세 정보 비동기 로딩 및 캐싱 로직 구현**
* `BadgeViewModel` 내에 `badgeDetailCache`를 도입하여 한 번 조회한 상세 정보는 다시 서버에 요청하지 않도록 개선했습니다.
* 뱃지 클릭 시 상세 정보를 가져오는 `fetchBadgeDetail` 로직과 모달 닫기 처리를 위한 `DismissBadgeDetail` 인텐트를 추가했습니다.
* 상세 정보 로딩 상태(`isBadgeDetailLoading`)와 실패 시 스낵바 메시지 처리를 추가했습니다.
* **refactor: 뱃지 화면 컴포넌트 분리 및 데이터 모델 최적화**
* 기존 `BadgeScreen`의 리스트 표시 로직을 `BadgeListContent`로 분리하여 가독성을 높였습니다.
* `BadgeUiModel`에서 상세 설명 필드를 제거하고, 상세 조회 전용 모델인 `BadgeDetailUiModel`을 정의하여 데이터 구조를 최적화했습니다.
* 프리뷰 데이터 관리를 위한 `BadgePreviewData.kt`를 추가했습니다.
* **style: 문자열 포맷팅 및 가독성 개선**
* 뱃지 상세 설명의 마침표(`.`) 뒤에 줄바꿈(`\n`)을 추가하여 텍스트 가독성을 높였습니다.
* **refactor: 뱃지 상세 정보 관련 상태 및 로딩 로직 제거**
* `BadgeUiState`에서 더 이상 사용하지 않는 `isBadgeDetailLoading` 필드를 제거했습니다.
* `BadgeViewModel` 내 뱃지 선택 및 상세 정보 조회 로직에서 로딩 상태를 업데이트하던 코드를 삭제했습니다.
* **ui: `BadgeDetailModal` 디자인 및 구조 개선**
* 상세 모달에서 `isLoading` 파라미터와 `CircularProgressIndicator`를 제거했습니다.
* 뱃지 상세 정보를 효과적으로 보여주기 위해 `BadgeDetailChip`과 `BadgeDetailDescription` 컴포저블을 새롭게 정의했습니다.
* 모달 내 레이아웃을 `Alignment.CenterHorizontally` 중심으로 재구성하고, 뱃지 이미지, 이름, 칩, 상세 설명 간의 간격을 디자인 시스템 가이드에 맞춰 조정했습니다.
* 뱃지 상세 설명 영역에 `PrezelTheme` 기반의 타이포그래피와 컬러를 적용했습니다.
* **refactor: 스플래시 화면 내비게이션 지연 처리 및 가시성 제어 로직 추가**
* 화면 전환 시 급격한 UI 변화를 방지하기 위해 500ms의 지연 시간(`SPLASH_NAVIGATION_DELAY_MILLIS`)을 도입했습니다.
* `screenVisibility` 상태를 추가하여 내비게이션이 시작될 때 스플래시 콘텐츠를 화면에서 제거하도록 수정했습니다.
* `NavigateToHome`, `NavigateToLogin` 등 모든 내비게이션 액션에 `navigateWithDelay` 공통 로직을 적용했습니다.
* **style: 마이페이지 배경색 적용**
* `MyScreen`의 루트 레이아웃에 `PrezelTheme.colors.bgRegular` 배경색을 추가하여 디자인 일관성을 확보했습니다.
* **refactor: `BadgeHeader` 컴포넌트 추출**
* `BadgeDetailModal` 내부에 구현되어 있던 뱃지 이미지, 이름, 상태 칩 표시 로직을 별도의 private `@Composable` 함수인 `BadgeHeader`로 분리했습니다.
* 메인 모달 레이아웃에서 헤더 영역과 상세 설명 영역의 논리적 구분을 명확히 하여 코드 가독성을 개선했습니다.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 Walkthrough워크스루배지 도메인·네트워크(SSE)·저장소·use case·feature UI를 추가하고 앱에서 이벤트 스트림을 수집해 스낵바로 알립니다. PrezelBadge를 URL 기반으로 전환하고 My/Splash 화면과 통합합니다. 변경사항배지 기능 추가
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (3)
Prezel/core/network/src/main/java/com/team/prezel/core/network/model/badge/BadgeEventResponse.kt (1)
7-19: ⚡ Quick win모든 필드가 nullable인 점을 검토해주세요.
SSE 이벤트 응답의 모든 필드가 nullable이고 기본값이 null입니다. 이는 다음을 의미합니다:
- 필수 필드 없이 빈 이벤트가 도메인 레이어까지 전달될 수 있음
- 하위 레이어에서 null 검증 및 에러 핸들링이 필수
최소한
badgeCode나message같은 핵심 필드는 non-nullable로 선언하거나, 파싱 단계에서 유효성 검증을 추가하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/badge/BadgeEventResponse.kt` around lines 7 - 19, BadgeEventResponse currently has all nullable fields which lets empty SSE events propagate; make core fields non-nullable (e.g., change badgeCode and/or message to non-nullable) or add a validation step in the parsing layer that constructs BadgeEventResponse and rejects/masks invalid events. Specifically, update the BadgeEventResponse model (fields badgeCode, message) to be required or implement a companion/factory/serializer validator that checks these fields during deserialization and returns an error or a well-formed default, so invalid events never reach the domain layer.Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/BadgeSseEventParser.kt (1)
18-18: ⚡ Quick winSSE 원문을 이 위치에서 info 로그로 남기지 않는 편이 좋습니다.
connect/ping까지 포함해서 모든 이벤트가 여기서 찍혀서 로그가 쉽게 과해지고, 서버 payload가 그대로 남습니다. 최소한 필터링 이후에 debug 레벨로 제한하거나 제거하는 쪽이 안전합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/BadgeSseEventParser.kt` at line 18, The info-level log call Timber.tag("SSE-TEST").i("$eventName: $data") in BadgeSseEventParser should be removed or downgraded and filtered: stop logging raw SSE payload at info level, filter out heartbeat/connect/ping events before logging, and if you need to keep logs for debugging use Timber.d and redact or summarize the data (e.g., eventName only or a truncated/parsed payload) in the method that handles SSE parsing (BadgeSseEventParser / the function containing the Timber call) so production logs are not noisy or leaking server payloads.Prezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/BadgeViewModel.kt (1)
107-113: ⚡ Quick win상세 설명의 마침표 일괄 치환은 원문을 깨뜨릴 수 있습니다.
replace(".", ".\n")는 소수점, 약어, 이미 포맷된 문자열까지 전부 변형합니다. 표시 포맷이 필요하더라도 mapper에서 원문을 파괴하지 말고, 문장 단위 포맷터를 별도로 두거나 서버 포맷을 그대로 사용하는 편이 안전합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Prezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/BadgeViewModel.kt` around lines 107 - 113, The mapping in BadgeDetail.toUiModel currently mutates the original detailDescription with detailDescription.replace(".", ".\n"), which can corrupt content (e.g., decimals, abbreviations); remove this global replace in the BadgeDetail.toUiModel mapper and instead preserve the original detailDescription when constructing BadgeDetailUiModel, and if line-breaking is required create a separate sentence-formatter utility (e.g., formatBadgeDescription()) or perform formatting on the presentation layer/server-side rather than in the mapper so the raw text is not altered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Prezel/app/src/main/java/com/team/prezel/ui/PrezelApp.kt`:
- Around line 260-293: LaunchedEffect is keyed only on isAuthenticated so the
coroutine captures stale shouldShowNavigationBar and navigateToBadge values; fix
by wrapping shouldShowNavigationBar and navigateToBadge with
rememberUpdatedState() inside ObserveBadgeEvents (e.g. val currentShowNav =
rememberUpdatedState(shouldShowNavigationBar) and val currentNavigate =
rememberUpdatedState(navigateToBadge)) and use currentShowNav.value for
useRaisedPosition and currentNavigate.value for onAction when calling
snackbarHostState.showPrezelSnackbar within the collect block that uses
connectBadgeEventStreamUseCase.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/client/HttpClientFactory.kt`:
- Around line 119-124: 현재 installSse 내부에서 maxReconnectionAttempts =
Int.MAX_VALUE 및 reconnectionTime = 1.seconds로 무제한/짧은 간격 재연결을 설정해 서버 부하와 배터리/네트워크
문제를 유발하므로, installSse에서 재연결 정책을 변경해 재연결 시도를 제한하거나 지수 백오프를 적용하도록 수정하세요; 예를 들어
HttpClientConfig<*>.installSse 내에서 maxReconnectionAttempts를 합리적 값(예: 5~10)으로 낮추고
reconnectionTime을 증가시키거나(초 단위) 초기 지연과 최대 지연을 갖는 지수 백오프 알고리즘으로 대체하며(최대 재시도 횟수와 최대
지연을 캡으로 둠), 필요한 경우 이 값들을 외부 설정으로 노출해 테스트와 조정이 가능하게 하세요 (참조: 함수 installSse, 속성
maxReconnectionAttempts, reconnectionTime).
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/BadgeRemoteDataSourceImpl.kt`:
- Around line 30-56: connectBadgeEventStream in BadgeRemoteDataSourceImpl
currently closes the callbackFlow on any non-CancellationException
(close(throwable)), which causes permanent termination on transient SSE/network
errors; modify the implementation to keep the flow open and perform
reconnect/backoff instead of calling close(throwable): catch non-cancellation
throwables, log/emit a recoverable error if needed, delay with exponential
backoff (or fixed retry delay using BADGE_SSE_RETRY_DELAY_MILLIS) and retry the
httpClient.serverSentEvents loop (or wrap the SSE call in a retry loop), only
close the flow on explicit cancellation, and ensure parsed events from
BadgeSseEventParser continue to be trySend(...) during reconnects.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/badge/BadgeEventResponse.kt`:
- Line 18: BadgeEventResponse의 rawData 프로퍼티에 `@SerialName` 어노테이션이 빠져 있어 다른 프로퍼티들과
불일치하므로 BadgeEventResponse 클래스에서 val rawData: String? = null에
`@SerialName`("rawData")를 추가하여 직렬화 이름을 명시하고 서버 JSON 키가 "rawData"와 일치하는지 확인하세요; 대상
식별자: rawData 프로퍼티 in BadgeEventResponse.
In
`@Prezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/BadgeScreen.kt`:
- Around line 38-39: Extract the two hardcoded Korean error strings used for
BadgeUiMessage.FETCH_DATA_FAILED and BadgeUiMessage.FETCH_BADGE_DETAIL_FAILED in
BadgeScreen.kt into string resources; add entries like badge_fetch_data_failed
and badge_fetch_detail_failed to the appropriate strings.xml (core/ui or
feature/badge/impl) and replace the literal strings in BadgeScreen.kt with calls
to context.getString(R.string.badge_fetch_data_failed) and
context.getString(R.string.badge_fetch_detail_failed) (or use the existing
Compose stringResource helper if in a composable) so the UI uses the new
resource IDs instead of hardcoded text.
In
`@Prezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/BadgeViewModel.kt`:
- Around line 81-85: The onFailure block in BadgeViewModel always sends
BadgeUiEffect.ShowMessage(FETCH_BADGE_DETAIL_FAILED) even for stale requests;
change the logic in the onFailure handler that references
currentState.selectedBadgeCode and badgeCode so both dismissBadgeDetail() and
sendEffect(BadgeUiEffect.ShowMessage(BadgeUiMessage.FETCH_BADGE_DETAIL_FAILED))
are executed only when currentState.selectedBadgeCode == badgeCode (i.e., verify
the failing request's badgeCode matches the currently selected badge before
dismissing or emitting the error effect).
In
`@Prezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/component/BadgeDetailImage.kt`:
- Around line 48-55: The early return in BadgeDetailImage when isError is true
prevents the lock overlay from rendering; remove the return@Box so that after
drawing the fallback PrezelImage (resId = PrezelIcons.WarningCircleOutlined) the
function continues and the subsequent !isUnlocked overlay logic still executes,
i.e., render the fallback image but do not exit the Box early so the lock
overlay code that checks isUnlocked runs as intended.
In
`@Prezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/component/BadgeDetailModal.kt`:
- Around line 121-122: 칩에 현재 badgeName을 중복으로 표시하고 있으니 PrezelChip 호출부에서
detail.badgeName 대신 상세 UI 모델의 조건 텍스트를 사용하도록 변경하세요: 찾을 식별자는 PrezelChip(...) 호출과
전달되는 매개변수 text이며, 대체 값은 detail.conditionText(또는 UI 모델에서 해당 필드명)입니다; 컴포넌트가 null/빈
문자열을 안전하게 처리하도록 필요하면 빈값 방어(예: elvis 연산자 또는 isNullOrBlank 체크)를 추가해 주세요.
In
`@Prezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/component/BadgeListContent.kt`:
- Around line 37-44: Replace the hardcoded Korean strings in the PrezelTopAppBar
usage with string resources: change the title Text("나의 뱃지") to Text(text =
stringResource(R.string.your_badges_title)) and replace the Icon
contentDescription "뒤로가기" with stringResource(R.string.back_button_description);
update or add the corresponding entries in strings.xml (e.g., your_badges_title
and back_button_description) and import androidx.compose.ui.res.stringResource
in BadgeListContent.kt so PrezelTopAppBar, its title Text and Icon use localized
resources instead of literals.
In
`@Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashScreen.kt`:
- Around line 49-52: navigateWithDelay 내에서 screenVisibility = false 설정 직후 같은
코루틴에서 바로 navigate()를 호출하면 한 프레임의 재구성이 보장되지 않아 화면에 숨김 상태가 반영되지 않을 수 있습니다;
navigateWithDelay 함수에서 screenVisibility = false 이후 한 프레임을 건너뛰도록
awaitFrame/withFrameNanos 또는 짧은 delay(16) 등을 사용해 재구성 기회를 주거나, 대신 visibility 제어를
상위 AnimatedVisibility로 옮겨 navigate()를 호출하기 전에 화면이 실제로 숨겨지도록 처리하세요; 대상 식별자는
navigateWithDelay, screenVisibility, navigate()입니다.
---
Nitpick comments:
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/BadgeSseEventParser.kt`:
- Line 18: The info-level log call Timber.tag("SSE-TEST").i("$eventName: $data")
in BadgeSseEventParser should be removed or downgraded and filtered: stop
logging raw SSE payload at info level, filter out heartbeat/connect/ping events
before logging, and if you need to keep logs for debugging use Timber.d and
redact or summarize the data (e.g., eventName only or a truncated/parsed
payload) in the method that handles SSE parsing (BadgeSseEventParser / the
function containing the Timber call) so production logs are not noisy or leaking
server payloads.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/model/badge/BadgeEventResponse.kt`:
- Around line 7-19: BadgeEventResponse currently has all nullable fields which
lets empty SSE events propagate; make core fields non-nullable (e.g., change
badgeCode and/or message to non-nullable) or add a validation step in the
parsing layer that constructs BadgeEventResponse and rejects/masks invalid
events. Specifically, update the BadgeEventResponse model (fields badgeCode,
message) to be required or implement a companion/factory/serializer validator
that checks these fields during deserialization and returns an error or a
well-formed default, so invalid events never reach the domain layer.
In
`@Prezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/BadgeViewModel.kt`:
- Around line 107-113: The mapping in BadgeDetail.toUiModel currently mutates
the original detailDescription with detailDescription.replace(".", ".\n"), which
can corrupt content (e.g., decimals, abbreviations); remove this global replace
in the BadgeDetail.toUiModel mapper and instead preserve the original
detailDescription when constructing BadgeDetailUiModel, and if line-breaking is
required create a separate sentence-formatter utility (e.g.,
formatBadgeDescription()) or perform formatting on the presentation
layer/server-side rather than in the mapper so the raw text is not altered.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ed3a0456-91a9-483f-9db1-17c5bdca71e1
📒 Files selected for processing (52)
Prezel/app/build.gradle.ktsPrezel/app/src/main/java/com/team/prezel/MainActivity.ktPrezel/app/src/main/java/com/team/prezel/ui/PrezelApp.ktPrezel/app/src/main/java/com/team/prezel/ui/PrezelAppState.ktPrezel/app/src/main/res/values/strings.xmlPrezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/repository/BadgeRepositoryImpl.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/badge/BadgeRepository.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/badge/ConnectBadgeEventStreamUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/badge/FetchBadgeDetailUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/badge/FetchBadgesUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/FetchUserBadgesUseCase.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/badge/Badge.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/client/HttpClientFactory.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/BadgeRemoteDataSource.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/BadgeRemoteDataSourceImpl.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/BadgeSseEventParser.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/DataSourceModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/badge/BadgeEventResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/badge/GetBadgeDetailResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/badge/GetBadgeResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/service/BadgeService.ktPrezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.ktPrezel/core/ui/src/main/res/drawable/badge_start.xmlPrezel/core/ui/src/main/res/values/strings.xmlPrezel/feature/badge/api/build.gradle.ktsPrezel/feature/badge/api/src/main/java/com/team/prezel/feature/badge/api/BadgeNavKey.ktPrezel/feature/badge/impl/build.gradle.ktsPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/BadgeScreen.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/BadgeViewModel.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/component/BadgeDetailImage.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/component/BadgeDetailInfo.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/component/BadgeDetailModal.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/component/BadgeListContent.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/component/BadgePreviewData.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/contract/BadgeUiEffect.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/contract/BadgeUiIntent.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/contract/BadgeUiState.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/model/BadgeDetailUiModel.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/model/BadgeUiMessage.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/model/BadgeUiModel.ktPrezel/feature/badge/impl/src/main/java/com/team/prezel/feature/badge/impl/navigation/BadgeEntryBuilder.ktPrezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/HomeAnalysisFabOverlay.ktPrezel/feature/my/impl/build.gradle.ktsPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/model/BadgeUiModel.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.ktPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashScreen.ktPrezel/settings.gradle.kts
💤 Files with no reviewable changes (2)
- Prezel/core/ui/src/main/res/drawable/badge_start.xml
- Prezel/core/ui/src/main/res/values/strings.xml
* **fix: `LaunchedEffect` 내 상태 캡처 현상 해결을 위한 `rememberUpdatedState` 도입**
* `LaunchedEffect` 내부의 비동기 로직에서 `shouldShowNavigationBar` 상태를 참조할 때 최신 값을 보장하기 위해 `rememberUpdatedState`를 사용하도록 수정했습니다.
* 이를 통해 하단 네비게이션 바의 가시성 상태가 변경되어도 스낵바의 위치(`useRaisedPosition`)가 올바르게 계산되도록 개선했습니다.
* **refactor: `BadgeRemoteDataSourceImpl` 내 스트림 유지 및 재연결 로직 강화**
* SSE 스트림 생성 로직을 `createStreamJob` 메서드로 추출하여 가독성을 높였습니다.
* `while (isActive)` 루프와 `delay`를 도입하여, `CancellationException` 이외의 에러 발생 시 지정된 시간 이후 자동으로 연결을 재시도하도록 개선했습니다.
* 에러 발생 시 `Timber`를 통한 로그 기록을 추가했습니다.
* **refactor: `HttpClientFactory` 내 SSE 기본 설정 변경**
* `maxReconnectionAttempts`를 `Int.MAX_VALUE`에서 `10`으로 하향 조정했습니다.
* `reconnectionTime`을 `1.seconds`에서 `5.seconds`로 변경하여 재연결 간격을 늘렸습니다.
* **style: `BadgeEventResponse` 모델 개선**
* `rawData` 프로퍼티에 명시적으로 `@SerialName("rawData")` 어노테이션을 추가했습니다.
* **refactor: `feature:badge:impl` 모듈 내 문자열 리소스 관리**
* `strings.xml`을 신규 생성하여 화면 타이틀, 내비게이션 아이콘 설명, 데이터 요청 실패 메시지 등을 정의했습니다.
* `BadgeScreen`에서 `BadgeUiMessage`에 따라 스낵바를 표시할 때 하드코딩된 문자열 대신 리소스에서 가져온 메시지를 사용하도록 변경했습니다.
* `BadgeListContent` 및 `BadgeDetailModal`의 TopAppBar 타이틀과 아이콘 `contentDescription`에 `stringResource`를 적용했습니다.
* **refactor: `BadgeScreen` 내 `LaunchedEffect` 종속성 업데이트**
* UI Effect를 수집하는 `LaunchedEffect`의 키에 `viewModel`과 새로 정의된 문자열 리소스 변수들을 추가하여 안정성을 높였습니다.
* **refactor: `BadgeViewModel` 내 에러 메시지 노출 조건 강화**
* 뱃지 상세 정보 조회 실패 시, 현재 선택된 뱃지(`selectedBadgeCode`)와 요청한 뱃지 코드가 일치하는 경우에만 에러 메시지를 표시하고 상세 뷰를 닫도록 로직을 개선했습니다.
* **feat: 뱃지 이미지 로드 에러 스낵바 알림 추가**
* 이미지 로드 실패 시 표시할 에러 메시지(`badge_image_load_failed`)를 추가했습니다.
* `BadgeScreen`에서 `rememberCoroutineScope`와 `LocalSnackbarHostState`를 활용하여 이미지 로드 실패 시 스낵바를 노출하는 로직을 구현했습니다.
* **refactor: `BadgeDetailImage` 에러 처리 방식 개선**
* 이미지 로드 실패 시 컴포넌트 내부에서 경고 아이콘을 보여주던 기존 방식 대신, `onError` 콜백을 통해 이벤트를 상위로 전파하도록 변경했습니다.
* `LaunchedEffect`와 `hasReportedError` 상태를 도입하여 동일한 `imageUrl`에 대해 에러 콜백이 중복 호출되지 않도록 로직을 개선했습니다.
* **refactor: `BadgeDetailModal` 및 하위 컴포넌트 리팩터링**
* `BadgeDetailModal`과 `BadgeHeader`가 이미지 로드 실패 이벤트를 처리할 수 있도록 콜백을 추가했습니다.
* `BadgeDetailChip`이 `BadgeDetailUiModel` 전체를 의존하는 대신, 필요한 정보인 `badgeCondition` 문자열만 직접 전달받도록 수정했습니다.
* 뱃지 상세 칩에 표시되는 텍스트를 기존 `badgeName`에서 `conditionText`로 변경하여 정보를 명확히 했습니다.
📌 작업 내용
feature:badge:api,feature:badge:impl모듈을 추가하고 앱 내 navigation entry를 등록했습니다.🧩 관련 이슈
📸 스크린샷
📢 논의하고 싶은 내용
Summary by CodeRabbit