Skip to content

공통 발표 그래프 컴포넌트 구현#133

Open
moondev03 wants to merge 14 commits into
developfrom
feat/#120-공통-발표-그래프-컴포넌트-구현

Hidden character warning

The head ref may contain hidden characters: "feat/#120-\uacf5\ud1b5-\ubc1c\ud45c-\uadf8\ub798\ud504-\ucef4\ud3ec\ub10c\ud2b8-\uad6c\ud604"
Open

공통 발표 그래프 컴포넌트 구현#133
moondev03 wants to merge 14 commits into
developfrom
feat/#120-공통-발표-그래프-컴포넌트-구현

Conversation

@moondev03
Copy link
Copy Markdown
Member

@moondev03 moondev03 commented May 16, 2026

📌 작업 내용

홈, 분석 리포트 화면에서 사용될 공통 그래프 컴포넌트를 구현했습니다.


🧩 관련 이슈


📸 스크린샷

CardGraph

2026-05-16.23.14.25.mov

SpeedGraph

image

StickGraph

image

📢 논의하고 싶은 내용

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 연습 이력을 날짜 기반 스탬프로 추가 시각화하는 연습 추적 카드 출시
    • 발화 및 대본 일치율을 선 그래프로 표시하는 성능 분석 차트 추가
    • 음성 속도를 게이지 형태로 표시하는 속도 그래프 도입
    • 맞춤법 및 주술호응 오류 분포를 막대 그래프로 시각화
    • 새로운 보고서 기능 페이지 추가

Review Change Stack

moondev03 added 14 commits May 11, 2026 17:43
* **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` 데이터 모델을 정의했습니다.
@moondev03 moondev03 self-assigned this May 16, 2026
@moondev03 moondev03 requested a review from HamBeomJoon as a code owner May 16, 2026 14:17
@moondev03 moondev03 added the ✨ feat 새로운 기능 추가 또는 기존 기능 확장 label May 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

📝 Walkthrough

Walkthrough

PR은 core/ui 모듈에 네 가지 그래프 컴포넌트(PracticeCard, CardGraph, SpeedGraph, StickGraph)를 추가하고, 새로운 feature/report 모듈을 생성하여 보고 화면을 구현합니다. 필요한 라이브러리 의존성을 추가하고 코드 검사 규칙을 업데이트합니다.

Changes

UI 그래프 컴포넌트

Layer / File(s) Summary
의존성 및 설정 추가
Prezel/core/ui/build.gradle.kts, Prezel/detekt-config.yml
kotlinx.collections.immutable과 kotlinx.datetime 라이브러리를 core/ui에 추가하고, detekt 설정에서 BasicPreview와 LargeDevicePreview 주석을 함수 수 규칙 제외 목록에 추가합니다.
PracticeCard 컴포넌트: 연습 스탬프 추적
Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PracticeCard.kt
기준일 기준으로 연습 기록을 스탬프(EMPTY/AFTER/BEFORE 타입)로 표시하며, 페이지네이션, 날짜 포맷팅, 상태별 시각화, 선택적 "연습하기" 버튼을 제공합니다.
CardGraph 컴포넌트: 이중 시리즈 선 그래프
Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/CardGraph.kt
발화와 대본 일치율 두 데이터 시리즈를 선으로 시각화하며, 선택 인덱스 기반 가이드/마커, x축 중심 좌표 측정, 탭 제스처 처리, 조건부 스크롤 및 상세 정보 렌더링을 구현합니다.
SpeedGraph 컴포넌트: 호형 속도 게이지
Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/SpeedGraph.kt
사용자 속도를 호형 게이지로 렌더링하며, 기준 범위와 좋은 범위를 구분하고, 범위에 따라 게이지 색상을 결정하며, 각도 계산 및 범위 교집합 로직을 포함합니다.
StickGraph 컴포넌트: 막대 그래프
Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/StickGraph.kt
맞춤법(SPELLING)과 주술호응(GRAMMAR) 유형을 막대로 표시하며, 타입별 데이터 집계, 최대값 대비 비율 계산, 테마 색상 및 리소스 라벨 매핑을 구현합니다.
그래프 관련 문자열 리소스
Prezel/core/ui/src/main/res/values/strings.xml
StickGraph 및 CardGraph 관련 5개의 UI 라벨과 포맷 리소스를 추가합니다.

Report 기능 모듈

Layer / File(s) Summary
API 모듈 설정 및 네비게이션 키
Prezel/feature/report/api/build.gradle.kts, Prezel/feature/report/api/proguard-rules.pro, Prezel/feature/report/api/consumer-rules.pro, Prezel/feature/report/api/src/main/java/com/team/prezel/feature/report/api/ReportNavKey.kt
feature/report/api 모듈을 설정하고 @Serializable 직렬화 가능 ReportNavKey 네비게이션 키를 정의합니다.
impl 모듈 설정 및 UI 상태 계약
Prezel/feature/report/impl/build.gradle.kts, Prezel/feature/report/impl/proguard-rules.pro, Prezel/feature/report/impl/consumer-rules.pro, Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiState.kt, Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiIntent.kt, Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiEffect.kt
feature/report/impl 모듈을 설정하고 api 모듈 의존성을 추가한 후, UI 상태(Loading/Content)와 인텐트, 이펙트를 정의합니다.
ViewModel 및 Screen 구현
Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/ReportViewModel.kt, Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/ReportScreen.kt
Hilt 주입 가능한 ReportViewModel을 구현하고 초기 상태를 Loading에서 Content로 전환하며, 상태에 따라 로딩 또는 타이틀 텍스트를 표시하는 ReportScreen을 구성합니다.
네비게이션 등록 및 리소스
Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/navigation/ReportEntryBuilder.kt, Prezel/feature/report/impl/src/main/res/values/strings.xml
Hilt 멀티바인딩을 통해 ReportNavKeyReportScreen으로 등록하는 네비게이션 빌더를 구성하고, 화면 로딩 문구와 제목 리소스를 추가합니다.
Gradle 모듈 통합
Prezel/settings.gradle.kts
:feature:report:api:feature:report:impl 모듈을 프로젝트의 자동 포함 목록에 추가합니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '공통 발표 그래프 컴포넌트 구현'은 변경 사항의 핵심(CardGraph, SpeedGraph, StickGraph 등 그래프 컴포넌트 구현)을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 작업 내용, 관련 이슈(#120), 스크린샷을 포함하고 있으나, 논의하고 싶은 내용 섹션이 비어 있습니다. 필수 요소는 대부분 포함되어 있습니다.
Linked Issues check ✅ Passed PR에서 구현된 CardGraph, SpeedGraph, StickGraph 등 모든 그래프 컴포넌트는 이슈 #120의 SCR-REPORT-04, SCR-REPORT-NO-SCRIPT-04 식별자에 해당하는 공통 발표 그래프 컴포넌트 구현 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed build.gradle.kts의 의존성 추가, detekt-config.yml의 설정 변경, strings.xml의 리소스 추가 등은 모두 그래프 컴포넌트 구현 및 리포트 모듈 통합을 위한 범위 내 변경입니다.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
Prezel/detekt-config.yml (1)

49-53: 💤 Low value

FunctionNaming 규칙에도 프리뷰 어노테이션 추가 고려

BasicPreviewLargeDevicePreview가 다른 규칙들(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

📥 Commits

Reviewing files that changed from the base of the PR and between 2d696bc and f314efb.

📒 Files selected for processing (22)
  • Prezel/core/ui/build.gradle.kts
  • Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PracticeCard.kt
  • Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/CardGraph.kt
  • Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/SpeedGraph.kt
  • Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/graph/StickGraph.kt
  • Prezel/core/ui/src/main/res/values/strings.xml
  • Prezel/detekt-config.yml
  • Prezel/feature/report/api/build.gradle.kts
  • Prezel/feature/report/api/consumer-rules.pro
  • Prezel/feature/report/api/proguard-rules.pro
  • Prezel/feature/report/api/src/main/java/com/team/prezel/feature/report/api/ReportNavKey.kt
  • Prezel/feature/report/impl/build.gradle.kts
  • Prezel/feature/report/impl/consumer-rules.pro
  • Prezel/feature/report/impl/proguard-rules.pro
  • Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/ReportScreen.kt
  • Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/ReportViewModel.kt
  • Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiEffect.kt
  • Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiIntent.kt
  • Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/ReportUiState.kt
  • Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/navigation/ReportEntryBuilder.kt
  • Prezel/feature/report/impl/src/main/res/values/strings.xml
  • Prezel/settings.gradle.kts

Comment on lines +124 to +131
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),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +237 to +242
Text(
text = "spm",
modifier = Modifier.height(18.dp),
style = PrezelTheme.typography.caption1Regular,
color = PrezelTheme.colors.textSmall,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

단위 텍스트도 문자열 리소스로 분리해 주세요.

"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.

Comment on lines +54 to +56
require(data.size == StickGraphItemType.entries.size) {
"StickGraph 데이터는 각 항목별로 1개씩, 총 ${StickGraphItemType.entries.size}개가 필요합니다."
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

입력 검증이 타입 중복 케이스를 놓치고 있습니다.

현재는 개수만 검사해서 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.

Comment on lines +122 to +125
private fun Int.toStickHeight(maxCount: Int): Dp {
val heightRatio = this.toFloat() / maxCount
return (heightRatio * STICK_GRAPH_MAX_HEIGHT).dp
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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 = "연습하기",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

하드코딩된 문구를 문자열 리소스로 분리해 주세요.

현재 사용자 노출 텍스트가 코드에 직접 들어가 있어 로컬라이징 누락 가능성이 큽니다. 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.

Comment on lines +291 to +294
Icon(
painter = painterResource(PrezelIcons.Check),
contentDescription = "",
tint = type.tintColor(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

장식용 아이콘은 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능 추가 또는 기존 기능 확장

Projects

None yet

Development

Successfully merging this pull request may close these issues.

공통 발표 그래프 컴포넌트 구현

1 participant