공통 발표 그래프 컴포넌트 구현#133
Hidden character warning
Conversation
* **feat: 리포트 기능의 `api` 및 `impl` 모듈 생성**
* `:feature:report:api`와 `:feature:report:impl` 모듈을 신규 생성하고 `settings.gradle.kts`에 등록했습니다.
* `api` 모듈에는 외부 노출을 위한 `ReportNavKey`를, `impl` 모듈에는 실제 화면 구현 및 관련 로직을 배치했습니다.
* **feat: 리포트 화면 및 ViewModel 기본 구조 구현**
* MVI 패턴을 위한 `ReportUiState`, `ReportUiIntent`, `ReportUiEffect` 인터페이스를 정의했습니다.
* `ReportViewModel`을 추가하여 초기 상태(`Loading`)에서 데이터 로드 완료(`Content`)로 전환되는 기본 로직을 작성했습니다.
* Compose를 사용하여 `Loading`과 `Content` 상태에 대응하는 `ReportScreen` UI를 구현했습니다.
* **feat: Navigation3 기반 내비게이션 연동**
* `ReportNavKey`를 정의하여 타입 안정성이 보장되는 내비게이션 경로를 추가했습니다.
* `ReportEntryBuilder`를 통해 `ActivityRetainedComponent` 수준에서 내비게이션 엔트리가 주입되도록 Hilt 모듈 설정을 완료했습니다.
* **style: 관련 리소스 추가**
* 리포트 화면에서 사용하는 로딩 및 타이틀 문자열 리소스를 추가했습니다.
* **feat: `SpeedGraph` 컴포넌트 신규 구현**
* 사용자의 속도(SPM)를 원형 게이지 형태로 시각화하는 `SpeedGraph` 컴포넌트를 추가했습니다.
* 전체 범위를 나타내는 `baseRange`와 권장 범위를 나타내는 `goodRange`를 설정할 수 있습니다.
* 유저의 현재 수치가 권장 범위 내에 있는지 여부에 따라 게이지 색상이 동적으로 변경되도록 구현했습니다.
* **feat: 그래프 구성을 위한 데이터 모델 및 UI 요소 추가**
* 그래프의 배경, 강조 범위, 게이지 색상을 설정할 수 있는 `SpeedGraphColors` 데이터 클래스를 정의했습니다.
* `Canvas`를 사용하여 아크(Arc) 형태의 트랙과 게이지를 드로잉하고, 하단에 최소/최대 수치 라벨을 배치했습니다.
* 그래프 중앙에 현재 SPM 수치와 단위를 표시하는 `SpmDisplay`를 추가했습니다.
* **refactor: 각도 계산 및 범위 처리를 위한 유틸리티 함수 구현**
* 수치를 그래프 상의 각도로 변환하는 `toAngle`, `sweepFromStart` 등의 확장 함수를 추가했습니다.
* `IntRange` 간의 교집합을 계산하는 `intersect` 및 스윕 각도 계산 로직을 포함했습니다.
* **refactor: SpeedGraph 레이아웃 유연성 확보 및 하드코딩된 수치 제거**
* `BoxWithConstraints`를 도입하여 부모 컨테이너의 너비(`maxWidth`)에 따라 하단 레이블의 패딩이 비율(`GRAPH_LABEL_HORIZONTAL_PADDING_RATIO`)에 맞춰 동적으로 조절되도록 수정했습니다.
* `SpeedGraphArc` 내부에서 하드코딩된 크기(`GRAPH_DIAMETER`) 대신 `Canvas`의 `size`를 기준으로 원의 직경과 위치를 계산하도록 변경하여 다양한 컴포넌트 크기에 대응할 수 있도록 했습니다.
* **refactor: 각도 계산 로직 및 관련 함수 단순화**
* 복잡한 계산식 대신 고정된 `START_ANGLE`(135도) 상수를 정의하여 시작 각도 관리 방식을 직관적으로 변경했습니다.
* `toAngle`, `sweepFromStart`, `sweepAngle` 등의 헬퍼 함수에서 불필요한 매개변수를 제거하고, `START_ANGLE`과 `FULL_CIRCLE_DEGREES`를 기반으로 로직을 일원화했습니다.
* `LocalDensity` 참조를 제거하고 `Canvas`의 `DrawScope` 내에서 픽셀 기반 계산을 수행하도록 최적화했습니다.
* **style: 상수 명명 규칙 및 프리뷰 코드 정리**
* 기본 범위 값 관련 상수명에 `DEFAULT_` 접두사를 추가하여 의미를 명확히 했습니다.
* `SpeedGraphPreview`에서 다양한 상태를 확인할 수 있도록 샘플 데이터를 업데이트했습니다.
* **docs: SpeedGraph 내 주요 함수 및 유틸리티 메서드 KDoc 추가**
* `toAngle`, `sweepAngle`, `intersect` 등 그래프 시각화를 위한 각도 및 범위 계산 로직에 설명을 추가하여 코드 이해도를 높였습니다.
* `SpmDisplay`, `RangeBounds` 등 내부 컴포저블 함수의 역할을 명시했습니다.
* **refactor: SpeedGraph Preview 구조 개선**
* 기존 하나의 Preview 함수에서 여러 상태를 한꺼번에 보여주던 방식을 `SpeedGraphGoodPreview`, `SpeedGraphSlowPreview`, `SpeedGraphFastPreview`로 개별 분리했습니다.
* 이를 통해 IDE 디자인 툴에서 각 속도 상태별(적정, 느림, 빠름) 그래프 모습을 개별적으로 확인하기 용이하도록 개선했습니다.
* **refactor: `SpeedGraph` 레이아웃 및 렌더링 방식 개선**
* `BoxWithConstraints`를 `Box`로 대체하고, `Canvas` 직접 호출 대신 `drawWithCache`를 사용하여 그리기 관련 객체 생성 비용을 최적화했습니다.
* 그래프 그리기 로직을 `SpeedGaugeArc` 컴포넌트로 분리하고, 중앙 수치 표시 로직을 `SpeedValueLabel`로 명확하게 명명했습니다.
* 하단 범위 레이블(`RangeBoundLabels`)의 너비 조절 방식을 `maxWidth` 기반의 동적 패딩 계산에서 `fillMaxWidth` 비율 기반으로 변경하여 레이아웃 구조를 단순화했습니다.
* **refactor: 코드 가독성 및 유틸리티 함수 개선**
* `toAngle`, `sweepAngle` 등의 헬퍼 함수를 `toGraphAngle`, `graphSweepAngle` 등으로 변경하여 그래프 각도 계산임을 명확히 했습니다.
* `Size.calculateGraphArcMetrics` 확장 함수를 추가하여 아크의 오프셋, 크기, 스트로크 계산 로직을 캡슐화했습니다.
* 불필요한 `hasValidBaseRange` 검사 로직을 정리하고, 각도 계산 시 발생할 수 있는 예외 케이스 처리를 보완했습니다.
* **feat: `StickGraph` 컴포넌트 및 데이터 모델 정의**
* 맞춤법(SPELLING) 및 주술호응(GRAMMAR) 통계를 시각화하는 `StickGraph` 컴포넌트를 추가했습니다.
* 데이터 전달을 위한 `StickData` 클래스와 항목 구분을 위한 `StickGraphItemType` Enum을 정의했습니다.
* 입력 데이터 중 최대 수치에 비례하여 막대 높이를 계산하는 `toStickHeight` 로직과 항목별 테마 색상 적용 로직을 구현했습니다.
* **build: `kotlinx-collections-immutable` 의존성 추가**
* Compose UI 컴포넌트의 안정성(Stability) 최적화를 위해 `ImmutableList`를 사용할 수 있도록 `core:ui` 모듈에 관련 라이브러리를 추가했습니다.
* **resource: 그래프 레이블용 문자열 리소스 추가**
* 그래프 하단에 표시될 '맞춤법' 및 '주술호응' 명칭에 대한 문자열 리소스를 추가했습니다.
* **feat: 커스텀 선형 차트 컴포넌트 `CardGraph` 추가**
* 발화율(`speech`)과 대본 일치율(`scriptMatch`) 데이터를 시각화하는 커스텀 라인 차트를 구현했습니다.
* `Canvas`를 사용하여 데이터 라인, 배경 그리드, 선택 마커 및 가이드 라인을 직접 드로잉했습니다.
* 데이터가 일정 개수(7개)를 초과할 경우 가로 스크롤이 가능하도록 구현했으며, 탭 제스처로 특정 시점의 데이터를 선택하는 인터랙션을 추가했습니다.
* **feat: 차트 상세 정보 및 범례 영역(`DetailContainer`) 구현**
* 차트 하단에 선택된 시점의 수치를 퍼센트(%)로 표시하거나, 전체 기간의 증감 수치를 퍼센트 포인트(%p)로 노출하는 범례 영역을 추가했습니다.
* `IntrinsicSize.Min`과 커스텀 디바이더를 활용하여 가변적인 텍스트 길이에 대응하는 레이아웃을 구성했습니다.
* **style: 컴포넌트 프리뷰 및 유틸리티 로직 구성**
* 좌표 기반의 가장 가까운 인덱스 탐색(`findClosestIndex`) 및 수치 포맷팅 함수를 정의했습니다.
* 기본, 선택 상태, 스크롤 가능 상태 등 다양한 케이스를 확인할 수 있는 Compose Preview를 추가했습니다.
* **refactor: `CardGraph` UI 로직 분리 및 모듈화**
* 전체 구조를 배경 및 컨테이너를 담당하는 `CardGraphContainer`와 실제 차트 내용을 담는 `CardGraphContent`로 분리했습니다.
* 가로 세로 비율을 상수로 관리하도록 `CARD_GRAPH_ASPECT_RATIO`를 추가했습니다.
* `items`가 비어있을 경우에 대한 에러 메시지를 구체화했습니다.
* **feat: 차트 표시 옵션 파라미터 추가**
* 하단 상세 정보를 제어하는 `showDetail` 파라미터를 추가했습니다.
* 컨테이너 스타일(배경색, 패딩 등) 적용 여부를 선택할 수 있는 `useContainerStyle` 파라미터를 추가했습니다.
* **refactor: 차트 데이터 계산 및 드로잉 로직 최적화**
* `Canvas` 내부에 복잡하게 구현되어 있던 좌표 계산 로직을 `toSeriesPoints`, `mapSeriesPoints` 등 별도의 유틸리티 함수로 추출했습니다.
* `toChartContentWidth`, `forEachValidCenter` 등의 확장 함수를 도입하여 코드 가독성을 높이고 중복 로직을 제거했습니다.
* 차트 시리즈 포인트를 관리하는 내부 데이터 클래스 `CardGraphSeriesPoints`를 추가했습니다.
* **test: 다양한 UI 케이스 확인을 위한 Preview 추가**
* 상세 정보가 없는 케이스와 컨테이너 스타일이 적용되지 않은 케이스에 대한 Preview를 추가했습니다.
* 인터랙티브 Preview에서 `PrezelChip`을 통해 실시간으로 옵션을 변경하며 테스트할 수 있도록 개선했습니다.
* **refactor: CardGraph 내부 상태 관리 및 드로잉 로직 개선**
* `CardGraphUiState`, `CardGraphColors`, `CardGraphDimensions` 등 내부 데이터 클래스를 정의하여 UI 상태와 드로잉 관련 설정을 체계화했습니다.
* 차트의 포인트 및 베이스라인 계산 로직을 `toChartState` 확장 함수로 분리하여 `LinearChart`의 가독성을 높였습니다.
* `XAxisRow`에서 직접 `MutableList`를 수정하던 방식을 `onChangePosition` 콜백을 통한 업데이트 방식으로 리팩터링했습니다.
* 가로 스크롤 시 `overscrollEffect = null`을 적용하여 시각적 일관성을 확보했습니다.
* **feat: UI 문자열 리소스화 및 다국어 대응 준비**
* 차트 X축 라벨("%1$d차"), 범례("발화", "대본 일치율"), 프리뷰용 칩 텍스트 등을 `strings.xml`로 추출했습니다.
* 컴포넌트 내 하드코딩된 문자열을 `stringResource` 사용으로 대체했습니다.
* **style: 코드 정리 및 프리뷰 최적화**
* 불필요한 상수(`CARD_GRAPH_PREVIEW_WIDTH`)를 제거하고 프리뷰 레이아웃에 `fillMaxWidth()`를 적용하여 반응형 구조로 변경했습니다.
* `ImmutableList` 활용을 강화하고 NaN 좌표 처리를 위한 `validCenterOrNull` 등 헬퍼 함수를 추가했습니다.
* 고정된 가로세로 비율(`CARD_GRAPH_ASPECT_RATIO`)을 `1.5f`로 단순화했습니다.
* **chore: `detekt-config.yml` 내 무시 어노테이션 설정 확장**
* `ignoreAnnotatedFunctions` 목록에 `BasicPreview` 및 `LargeDevicePreview`를 추가하여, 해당 어노테이션이 사용된 함수들이 검사 규칙(LongMethod 등)에서 제외되도록 수정했습니다.
* **refactor: `CardGraph` 컴포넌트 구조 및 레이아웃 최적화**
* `DetailContainer`를 가로 스크롤 영역 외부로 분리하여 항목 선택 시 상세 정보가 항상 화면에 보이도록 개선했습니다.
* `CardGraphContainer`의 `content` 파라미터에 `ColumnScope`를 적용하고, `Modifier.then()`을 사용하여 스타일 적용 로직을 간결화했습니다.
* `XAxisRow`에서 데이터가 1개인 경우 중앙 정렬(`Arrangement.Center`)이 적용되도록 수정했습니다.
* **refactor: 그래프 연산 및 그리기 로직 관심사 분리**
* **`CardGraphMath`**: 좌표 계산, 최접점 인덱스 찾기 등 수학적 연산 로직을 별도 객체로 분리했습니다.
* **`CardGraphDrawers`**: Canvas 그리기 로직을 가이드라인, 베이스라인, 시리즈, 마커 등으로 세분화하여 구조화했습니다.
* **`CardGraphTextFormatter`**: 퍼센트 및 퍼센트 포인트 표시 등 상세 정보 텍스트 포맷팅 로직을 캡슐화했습니다.
* **feat: 단일 데이터 포인트 시각화 지원**
* 데이터가 하나만 존재하여 선(Line)을 그릴 수 없는 경우에도 그래프상에 해당 지점을 확인할 수 있도록 `drawMarkerIfSinglePoint` 로직을 추가했습니다.
* **cleanup: 프리뷰 데이터 및 케이스 정비**
* 단일 항목, 7개 항목, 10개 항목 등 다양한 데이터셋에 대한 프리뷰를 추가하고, 중복되거나 불필요한 프리뷰 구성을 정리했습니다.
* **docs: `CardGraphDrawers` 내 주요 그리기 메서드에 KDoc 추가**
* 차트의 가이드라인, 기준선, 시리즈(데이터 선), 마커 등 각 드로잉 단계의 역할과 시각적 우선순위에 대한 설명을 추가했습니다.
* 데이터가 하나인 경우의 처리(`drawMarkerIfSinglePoint`) 및 선택 상태의 가이드 표현(`drawSelectedGuide`) 등 세부 로직에 주석을 보강했습니다.
* **style: `drawChart` 메서드 내 명시적 매개변수 이름 적용**
* 코드 가독성을 높이기 위해 `drawChart` 함수에서 하위 드로잉 함수를 호출할 때 모든 인자에 명시적 매개변수 이름(Named Arguments)을 사용하도록 수정했습니다.
* **refactor: `SpeedGraph` 내 그리기 로직 분리**
* `onDrawBehind` 블록 내부에 직접 작성되어 있던 `drawArc` 코드들을 기능별로 모듈화했습니다.
* `DrawScope`의 private 확장 함수인 `drawBaseTrack`, `drawGoodRange`, `drawUserGauge`를 추가하여 각 그래픽 레이어의 역할을 명확히 정의했습니다.
* 기존의 복잡한 매개변수 전달 구조를 유지하면서도, 함수 추출을 통해 코드의 가독성과 유지보수성을 높였습니다.
* **feat: 연습 진행 상황을 시각화하는 `PracticeCard` 컴포넌트 신규 개발**
* 날짜별 연습 완료 여부를 스탬프 형태로 표시하는 트래커 기능을 구현했습니다.
* 7일 단위의 페이징 처리를 지원하여 과거 및 향후 연습 계획을 확인할 수 있습니다.
* 연습 상태(`AFTER`, `BEFORE`, `EMPTY`)에 따라 배경색, 아이콘, 텍스트 색상 및 대시 보드 스타일이 동적으로 변경되도록 설계했습니다.
* **feat: `PracticeCard` 내 세부 UI 요소 및 데이터 모델 정의**
* 전체 연습 횟수 대비 완료 횟수를 표시하는 `CountRow`를 추가했습니다.
* 연습하기 동작을 수행하는 `PrezelTextButton` 기반의 액션 버튼을 포함했습니다.
* 외부에서 UI 상태를 주입받기 위한 `PracticeCardItem` 데이터 모델을 정의했습니다.
📝 WalkthroughWalkthroughPR은 core/ui 모듈에 네 가지 그래프 컴포넌트(PracticeCard, CardGraph, SpeedGraph, StickGraph)를 추가하고, 새로운 feature/report 모듈을 생성하여 보고 화면을 구현합니다. 필요한 라이브러리 의존성을 추가하고 코드 검사 규칙을 업데이트합니다. ChangesUI 그래프 컴포넌트
Report 기능 모듈
🚥 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: 6
🧹 Nitpick comments (1)
Prezel/detekt-config.yml (1)
49-53: 💤 Low valueFunctionNaming 규칙에도 프리뷰 어노테이션 추가 고려
BasicPreview와LargeDevicePreview가 다른 규칙들(UnusedPrivateMember,LongMethod,TooManyFunctions)의 제외 대상에는 추가되었으나,FunctionNaming.ignoreAnnotated에는 포함되지 않았습니다.프리뷰 함수가 비표준 네이밍을 사용하는 경우 일관성을 위해 추가를 고려해보세요.
♻️ 일관성을 위한 제안
naming: FunctionNaming: active: true ignoreAnnotated: - Composable - Preview + - BasicPreview + - LargeDevicePreview🤖 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/detekt-config.yml` around lines 49 - 53, FunctionNaming 규칙의 ignoreAnnotated 목록에 Preview 계열 어노테이션이 빠져 일관성이 깨집니다; FunctionNaming.ignoreAnnotated 설정에 Preview(및 프로젝트에서 사용하는 BasicPreview, LargeDevicePreview 같은 커스텀 프리뷰 어노테이션)를 추가하여 프리뷰 함수가 네이밍 규칙 위반으로 보고되지 않도록 업데이트하세요.
🤖 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/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/CardGraph.kt`:
- Around line 124-131: xAxisCenters is being copied every composition via
xAxisCenters.toImmutableList() which produces a new object used as a key and
forces pointerInput/gesture detectors to restart; stop creating a fresh list
each composition by producing a stable reference (either change
rememberCardGraphXAxisCenters to return an ImmutableList directly or wrap the
conversion in remember, e.g. remember(xAxisCenters) {
xAxisCenters.toImmutableList() }) and pass that stable list into
CardGraphUiState (and similarly replace the other occurrences around the 236-240
region) so the pointerInput key remains stable and gesture recognition isn't
frequently recreated.
In
`@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/SpeedGraph.kt`:
- Around line 237-242: Hardcoded unit text "spm" should be moved to resources;
replace the literal in the Text composable inside SpeedGraph.kt with a
stringResource lookup (use stringResource(R.string.spm)) and add a corresponding
entry in strings.xml (e.g., <string name="spm">spm</string>), and ensure you
import androidx.compose.ui.res.stringResource; keep the existing
modifiers/style/PrezelTheme usage unchanged.
In
`@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/StickGraph.kt`:
- Around line 54-56: The current validation in the StickGraph constructor/check
(the require call that compares data.size to StickGraphItemType.entries.size)
only checks count and misses duplicate types like two SPELLING entries; update
the validation to ensure the set of item types in data matches the expected set
in StickGraphItemType.entries (e.g., compare data.map { it.type }.toSet() to
StickGraphItemType.entries.toSet() or check both sizes and containsAll) so
duplicates are rejected and every expected type is present; apply the same
change to the second validation block around the code referenced at lines
112-120.
- Around line 122-125: toStickHeight 계산이 maxCount가 0일 때 0f/0으로 NaN을 만들 수 있으니
Int.toStickHeight 함수에 가드를 추가하여 maxCount가 0 이하일 경우 즉시 0.dp를 반환하도록 수정하고, 그렇지 않을 때만
기존 로직을 수행하도록 변경하세요; 참고 심볼: toStickHeight 함수와 STICK_GRAPH_MAX_HEIGHT 상수를 찾아
적용하세요.
In
`@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PracticeCard.kt`:
- Line 115: The PracticeCard composable in PracticeCard.kt currently uses
hardcoded Korean strings (e.g., the "연습하기" text and the other occurrences noted)
— replace each hardcoded user-facing string with stringResource(...) calls
(import androidx.compose.ui.res.stringResource and use
stringResource(R.string.<key>)) referencing new keys you add to your strings.xml
(e.g., practice_button_text, practice_title, practice_description,
practice_subtext or similar descriptive keys). Update all occurrences mentioned
(the text parameters at the identified locations) to use these keys and add the
corresponding localized entries in strings.xml (Korean and any existing locales)
to complete the refactor. Ensure keys are descriptive and unique to avoid
collisions.
- Around line 291-294: In PracticeCard.kt where the Icon composable (using
painterResource(PrezelIcons.Check) and tint = type.tintColor()) is used, change
the decorative icon's accessibility handling by setting contentDescription to
null instead of an empty string so the icon is treated as purely decorative by
the accessibility tree; locate the Icon invocation inside the PracticeCard
component and replace contentDescription = "" with contentDescription = null.
---
Nitpick comments:
In `@Prezel/detekt-config.yml`:
- Around line 49-53: FunctionNaming 규칙의 ignoreAnnotated 목록에 Preview 계열 어노테이션이 빠져
일관성이 깨집니다; FunctionNaming.ignoreAnnotated 설정에 Preview(및 프로젝트에서 사용하는
BasicPreview, LargeDevicePreview 같은 커스텀 프리뷰 어노테이션)를 추가하여 프리뷰 함수가 네이밍 규칙 위반으로
보고되지 않도록 업데이트하세요.
🪄 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: 8e15a16f-e42b-4328-a2c8-31749b96c756
📒 Files selected for processing (22)
Prezel/core/ui/build.gradle.ktsPrezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PracticeCard.ktPrezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/CardGraph.ktPrezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/SpeedGraph.ktPrezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/StickGraph.ktPrezel/core/ui/src/main/res/values/strings.xmlPrezel/detekt-config.ymlPrezel/feature/report/api/build.gradle.ktsPrezel/feature/report/api/consumer-rules.proPrezel/feature/report/api/proguard-rules.proPrezel/feature/report/api/src/main/java/com/team/prezel/feature/report/api/ReportNavKey.ktPrezel/feature/report/impl/build.gradle.ktsPrezel/feature/report/impl/consumer-rules.proPrezel/feature/report/impl/proguard-rules.proPrezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/ReportScreen.ktPrezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/ReportViewModel.ktPrezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiEffect.ktPrezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiIntent.ktPrezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiState.ktPrezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/navigation/ReportEntryBuilder.ktPrezel/feature/report/impl/src/main/res/values/strings.xmlPrezel/settings.gradle.kts
| val xAxisCenters = rememberCardGraphXAxisCenters(itemCount = items.size) | ||
|
|
||
| BoxWithConstraints { | ||
| val uiState = CardGraphUiState( | ||
| enableScroll = items.shouldEnableHorizontalScroll(), | ||
| contentWidth = maxWidth.toChartContentWidth(itemCount = items.size), | ||
| xAxisCenters = xAxisCenters.toImmutableList(), | ||
| selectedItemIndex = items.resolveSelectedItemIndex(selectedItemIndex), |
There was a problem hiding this comment.
pointerInput 키가 불안정해서 제스처 인식기가 자주 재시작될 수 있습니다.
xAxisCenters를 매번 복사(toImmutableList)해 key로 쓰면 불필요한 pointerInput 재생성이 발생합니다. 스크롤/탭 상호작용 안정성에 영향이 있습니다.
제안 수정안
private data class CardGraphUiState(
val enableScroll: Boolean,
val contentWidth: Dp,
- val xAxisCenters: ImmutableList<Float>,
+ val xAxisCenters: List<Float>,
val selectedItemIndex: Int?,
)
...
val uiState = CardGraphUiState(
enableScroll = items.shouldEnableHorizontalScroll(),
contentWidth = maxWidth.toChartContentWidth(itemCount = items.size),
- xAxisCenters = xAxisCenters.toImmutableList(),
+ xAxisCenters = xAxisCenters,
selectedItemIndex = items.resolveSelectedItemIndex(selectedItemIndex),
)
...
- modifier = modifier.pointerInput(uiState.xAxisCenters, items.size) {
+ modifier = modifier.pointerInput(items.size) {
detectTapGestures { tapOffset ->
with(CardGraphMath) {
uiState.xAxisCenters.findClosestIndex(tapOffset.x)?.let(onSelectItem)
}
}
},Also applies to: 236-240
🤖 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/ui/src/main/java/com/team/prezel/core/ui/component/graph/CardGraph.kt`
around lines 124 - 131, xAxisCenters is being copied every composition via
xAxisCenters.toImmutableList() which produces a new object used as a key and
forces pointerInput/gesture detectors to restart; stop creating a fresh list
each composition by producing a stable reference (either change
rememberCardGraphXAxisCenters to return an ImmutableList directly or wrap the
conversion in remember, e.g. remember(xAxisCenters) {
xAxisCenters.toImmutableList() }) and pass that stable list into
CardGraphUiState (and similarly replace the other occurrences around the 236-240
region) so the pointerInput key remains stable and gesture recognition isn't
frequently recreated.
| Text( | ||
| text = "spm", | ||
| modifier = Modifier.height(18.dp), | ||
| style = PrezelTheme.typography.caption1Regular, | ||
| color = PrezelTheme.colors.textSmall, | ||
| ) |
There was a problem hiding this comment.
단위 텍스트도 문자열 리소스로 분리해 주세요.
"spm" 하드코딩 대신 리소스로 관리하면 다국어/표기 변경 대응이 쉬워집니다.
제안 수정안
+import androidx.compose.ui.res.stringResource
+import com.team.prezel.core.ui.R
...
- Text(
- text = "spm",
+ Text(
+ text = stringResource(R.string.core_ui_impl_speed_graph_unit_spm),
modifier = Modifier.height(18.dp),
style = PrezelTheme.typography.caption1Regular,
color = PrezelTheme.colors.textSmall,
)+ <string name="core_ui_impl_speed_graph_unit_spm">spm</string>🤖 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/ui/src/main/java/com/team/prezel/core/ui/component/graph/SpeedGraph.kt`
around lines 237 - 242, Hardcoded unit text "spm" should be moved to resources;
replace the literal in the Text composable inside SpeedGraph.kt with a
stringResource lookup (use stringResource(R.string.spm)) and add a corresponding
entry in strings.xml (e.g., <string name="spm">spm</string>), and ensure you
import androidx.compose.ui.res.stringResource; keep the existing
modifiers/style/PrezelTheme usage unchanged.
| require(data.size == StickGraphItemType.entries.size) { | ||
| "StickGraph 데이터는 각 항목별로 1개씩, 총 ${StickGraphItemType.entries.size}개가 필요합니다." | ||
| } |
There was a problem hiding this comment.
입력 검증이 타입 중복 케이스를 놓치고 있습니다.
현재는 개수만 검사해서 SPELLING, SPELLING 같은 입력이 통과할 수 있고, 그 경우 집계 후 막대가 1개만 렌더링됩니다. 타입 집합 기준으로 검증해야 안전합니다.
제안 수정안
fun StickGraph(
data: ImmutableList<StickData>,
modifier: Modifier = Modifier,
) {
- require(data.size == StickGraphItemType.entries.size) {
- "StickGraph 데이터는 각 항목별로 1개씩, 총 ${StickGraphItemType.entries.size}개가 필요합니다."
- }
+ val requiredTypes = StickGraphItemType.entries.toSet()
+ val inputTypes = data.map(StickData::itemType).toSet()
+ require(inputTypes == requiredTypes) {
+ "StickGraph 데이터는 ${StickGraphItemType.entries.joinToString()} 타입을 각각 포함해야 합니다."
+ }Also applies to: 112-120
🤖 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/ui/src/main/java/com/team/prezel/core/ui/component/graph/StickGraph.kt`
around lines 54 - 56, The current validation in the StickGraph constructor/check
(the require call that compares data.size to StickGraphItemType.entries.size)
only checks count and misses duplicate types like two SPELLING entries; update
the validation to ensure the set of item types in data matches the expected set
in StickGraphItemType.entries (e.g., compare data.map { it.type }.toSet() to
StickGraphItemType.entries.toSet() or check both sizes and containsAll) so
duplicates are rejected and every expected type is present; apply the same
change to the second validation block around the code referenced at lines
112-120.
| private fun Int.toStickHeight(maxCount: Int): Dp { | ||
| val heightRatio = this.toFloat() / maxCount | ||
| return (heightRatio * STICK_GRAPH_MAX_HEIGHT).dp | ||
| } |
There was a problem hiding this comment.
maxCount == 0일 때 높이 계산이 깨질 수 있습니다.
모든 count가 0이면 0f / 0으로 NaN이 발생할 수 있어, 높이 계산에 가드를 두는 게 필요합니다.
제안 수정안
private fun Int.toStickHeight(maxCount: Int): Dp {
+ if (maxCount <= 0) return 0.dp
val heightRatio = this.toFloat() / maxCount
return (heightRatio * STICK_GRAPH_MAX_HEIGHT).dp
}🤖 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/ui/src/main/java/com/team/prezel/core/ui/component/graph/StickGraph.kt`
around lines 122 - 125, toStickHeight 계산이 maxCount가 0일 때 0f/0으로 NaN을 만들 수 있으니
Int.toStickHeight 함수에 가드를 추가하여 maxCount가 0 이하일 경우 즉시 0.dp를 반환하도록 수정하고, 그렇지 않을 때만
기존 로직을 수행하도록 변경하세요; 참고 심볼: toStickHeight 함수와 STICK_GRAPH_MAX_HEIGHT 상수를 찾아
적용하세요.
| Spacer(modifier = Modifier.height(PrezelTheme.spacing.V12)) | ||
|
|
||
| PrezelTextButton( | ||
| text = "연습하기", |
There was a problem hiding this comment.
하드코딩된 문구를 문자열 리소스로 분리해 주세요.
현재 사용자 노출 텍스트가 코드에 직접 들어가 있어 로컬라이징 누락 가능성이 큽니다. stringResource 기반으로 통일하는 게 안전합니다.
제안 수정안
+import androidx.compose.ui.res.stringResource
+import com.team.prezel.core.ui.R
...
- text = "연습하기",
+ text = stringResource(R.string.core_ui_impl_practice_card_action),
...
- contentDescription = "이전 페이지",
+ contentDescription = stringResource(R.string.core_ui_impl_practice_card_prev_page),
...
- text = "연습한 횟수",
+ text = stringResource(R.string.core_ui_impl_practice_card_count_label),
...
- contentDescription = "다음 페이지",
+ contentDescription = stringResource(R.string.core_ui_impl_practice_card_next_page),+ <string name="core_ui_impl_practice_card_action">연습하기</string>
+ <string name="core_ui_impl_practice_card_prev_page">이전 페이지</string>
+ <string name="core_ui_impl_practice_card_next_page">다음 페이지</string>
+ <string name="core_ui_impl_practice_card_count_label">연습한 횟수</string>Also applies to: 173-173, 180-180, 188-188
🤖 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/ui/src/main/java/com/team/prezel/core/ui/component/PracticeCard.kt`
at line 115, The PracticeCard composable in PracticeCard.kt currently uses
hardcoded Korean strings (e.g., the "연습하기" text and the other occurrences noted)
— replace each hardcoded user-facing string with stringResource(...) calls
(import androidx.compose.ui.res.stringResource and use
stringResource(R.string.<key>)) referencing new keys you add to your strings.xml
(e.g., practice_button_text, practice_title, practice_description,
practice_subtext or similar descriptive keys). Update all occurrences mentioned
(the text parameters at the identified locations) to use these keys and add the
corresponding localized entries in strings.xml (Korean and any existing locales)
to complete the refactor. Ensure keys are descriptive and unique to avoid
collisions.
| Icon( | ||
| painter = painterResource(PrezelIcons.Check), | ||
| contentDescription = "", | ||
| tint = type.tintColor(), |
There was a problem hiding this comment.
장식용 아이콘은 contentDescription = null로 처리해 주세요.
빈 문자열보다 null이 접근성 트리에서 더 명확한 장식 처리입니다.
제안 수정안
Icon(
painter = painterResource(PrezelIcons.Check),
- contentDescription = "",
+ contentDescription = null,
tint = type.tintColor(),
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Icon( | |
| painter = painterResource(PrezelIcons.Check), | |
| contentDescription = "", | |
| tint = type.tintColor(), | |
| Icon( | |
| painter = painterResource(PrezelIcons.Check), | |
| contentDescription = null, | |
| tint = type.tintColor(), |
🤖 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/ui/src/main/java/com/team/prezel/core/ui/component/PracticeCard.kt`
around lines 291 - 294, In PracticeCard.kt where the Icon composable (using
painterResource(PrezelIcons.Check) and tint = type.tintColor()) is used, change
the decorative icon's accessibility handling by setting contentDescription to
null instead of an empty string so the icon is treated as purely decorative by
the accessibility tree; locate the Icon invocation inside the PracticeCard
component and replace contentDescription = "" with contentDescription = null.
📌 작업 내용
홈, 분석 리포트 화면에서 사용될 공통 그래프 컴포넌트를 구현했습니다.
🧩 관련 이슈
📸 스크린샷
CardGraph
2026-05-16.23.14.25.mov
SpeedGraph
StickGraph
📢 논의하고 싶은 내용
Summary by CodeRabbit
릴리스 노트