Tracker#
여러 세그먼트로 구성된 상태를 색상으로 시각화하는 컴포넌트입니다. 업타임 모니터링, 습관 추적, 스트릭 표시 등에 활용됩니다.
Live Preview#
class TrackerDefaultExample extends StatefulComponent {
const TrackerDefaultExample({super.key});
@override
State<TrackerDefaultExample> createState() => _TrackerDefaultExampleState();
}
class _TrackerDefaultExampleState extends State<TrackerDefaultExample> {
@override
Component build(BuildContext context) {
return Tracker(
segments: [
CoreTrackerSegment(status: CoreTrackerStatus.fine, tooltip: 'OK'),
CoreTrackerSegment(status: CoreTrackerStatus.warning, tooltip: 'Slow'),
CoreTrackerSegment(status: CoreTrackerStatus.critical, tooltip: 'Down'),
],
);
}
}
class TrackerDefaultExample extends StatefulWidget {
const TrackerDefaultExample({super.key});
@override
State<TrackerDefaultExample> createState() => _TrackerDefaultExampleState();
}
class _TrackerDefaultExampleState extends State<TrackerDefaultExample> {
@override
Widget build(BuildContext context) {
return Tracker(
segments: [
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.critical),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.unknown),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
],
);
}
}
class TrackerChainExample extends StatefulComponent {
const TrackerChainExample({super.key});
@override
State<TrackerChainExample> createState() => _TrackerChainExampleState();
}
class _TrackerChainExampleState extends State<TrackerChainExample> {
@override
Component build(BuildContext context) {
return Tracker(
segments: [
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.critical),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.unknown),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
],
).withStyle(
const CoreTrackerStyle(
itemHeight: CoreSpace.space48,
itemSpacing: CoreSpace.space4,
endRadius: CoreRadius.radius8,
),
);
}
}
class TrackerChainExample extends StatefulWidget {
const TrackerChainExample({super.key});
@override
State<TrackerChainExample> createState() => _TrackerChainExampleState();
}
class _TrackerChainExampleState extends State<TrackerChainExample> {
@override
Widget build(BuildContext context) {
return Tracker(
segments: [
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.critical),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.unknown),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
],
).withStyle(
const CoreTrackerStyle(
itemHeight: CoreSpace.space48,
itemSpacing: CoreSpace.space4,
endRadius: CoreRadius.radius8,
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 서비스 업타임/다운타임 이력을 색상 바로 표시할 때
- 습관 추적, 스트릭, 연속 기록을 시각화할 때
대신 다른 컴포넌트를 사용하세요:
Progress: 단일 진행률 표시Stat: 숫자 기반 통계 표시
기본 사용법 (Basic Usage)#
Tracker(
segments: [
CoreTrackerSegment(status: CoreTrackerStatus.fine, tooltip: 'OK'),
CoreTrackerSegment(status: CoreTrackerStatus.warning, tooltip: 'Slow'),
CoreTrackerSegment(status: CoreTrackerStatus.critical, tooltip: 'Down'),
CoreTrackerSegment(status: CoreTrackerStatus.unknown, tooltip: 'N/A'),
],
)
Tracker(
segments: [
CoreTrackerSegment(status: CoreTrackerStatus.fine, tooltip: 'OK'),
CoreTrackerSegment(status: CoreTrackerStatus.warning, tooltip: 'Slow'),
CoreTrackerSegment(status: CoreTrackerStatus.critical, tooltip: 'Down'),
CoreTrackerSegment(status: CoreTrackerStatus.unknown, tooltip: 'N/A'),
],
)
빠른 오버라이드 (Chain)#
이미 만든 Tracker 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class TrackerChainExample extends StatefulWidget {
const TrackerChainExample({super.key});
@override
State<TrackerChainExample> createState() => _TrackerChainExampleState();
}
class _TrackerChainExampleState extends State<TrackerChainExample> {
@override
Widget build(BuildContext context) {
return Tracker(
segments: [
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.critical),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.unknown),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
],
).withStyle(
const CoreTrackerStyle(
itemHeight: CoreSpace.space48,
itemSpacing: CoreSpace.space4,
endRadius: CoreRadius.radius8,
),
);
}
}
class TrackerChainExample extends StatefulComponent {
const TrackerChainExample({super.key});
@override
State<TrackerChainExample> createState() => _TrackerChainExampleState();
}
class _TrackerChainExampleState extends State<TrackerChainExample> {
@override
Component build(BuildContext context) {
return Tracker(
segments: [
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.critical),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.unknown),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.warning),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
CoreTrackerSegment(status: CoreTrackerStatus.fine),
],
).withStyle(
const CoreTrackerStyle(
itemHeight: CoreSpace.space48,
itemSpacing: CoreSpace.space4,
endRadius: CoreRadius.radius8,
),
);
}
}
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
segments |
List<CoreTrackerSegment> |
필수 | 세그먼트 데이터 목록 |
rounded | bool | true | 모서리 둥글기 적용 |
trackerStyle |
CoreTrackerStyle? |
null |
인스턴스 chrome/dimensional override |
CoreTrackerStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
itemHeight |
double? |
Height of each tracker segment (logical px). |
itemSpacing |
double? |
Horizontal spacing (logical px, pre-scaling) between adjacent tracker segments. Flows directly into
Row.spacing
(Flutter native paint-API argument, 3.27+) on Flutter and the outer flex row's CSS
gap
inline rule on Web — both are native paint-API consumers that take a raw double, so this slot stays scalar (
double?
) rather than the nested
CoreGapStyle
shape used for
Gap
slots. Falls back to [defaultItemSpacing] when null.
|
endRadius |
double? |
Border-radius applied to the outer ends of the segment row when
widget.rounded == true
(logical px).
|
segmentColor |
CoreColor? |
Optional global override applied to every segment, regardless of the segment's [CoreTrackerStatus]. When
null
(the default) the resolver falls back to the per-status colour in [defaultsByStatus].
|
CoreTrackerSegment#
| 속성 | 타입 | 설명 |
|---|---|---|
status |
CoreTrackerStatus |
상태 (fine, warning, critical, unknown) |
tooltip | String? | 호버 시 툴팁 텍스트 |
CoreTrackerStatus 색상#
| 상태 | 색상 |
|---|---|
fine | success (green) |
warning | warning (orange) |
critical | error (red) |
unknown | outlineVariant (gray) |
사용 가이드라인 (Usage Guidelines)#
✅ Do#
세그먼트마다 의미 있는 tooltip 지정
CoreTrackerSegment(status: CoreTrackerStatus.critical, tooltip: '다운타임 발생')
이유: tooltip을 생략하면 접근 가능한 이름이 로컬라이즈되지 않은 CoreTrackerStatus enum 이름(critical 등)으로 대체되어, 스크린 리더 사용자에게 상태 의미가 전달되지 않습니다.
❌ Don't#
요약 정보 없이 Tracker 단독 배치 금지
// ❌ "12개 중 3개 위험" 같은 요약 없이 Tracker 만 배치
Tracker(segments: uptimeSegments)
이유: 세그먼트는 hover 로만 열리는 툴팁을 가진 비인터랙티브 요소라 키보드 사용자는 개별 상태를 확인할 방법이 없습니다. 항목 수·위험 개수 같은 요약 정보는 호출자가 별도 텍스트로 제공해야 합니다.
접근성 (Accessibility)#
역할 / Semantics#
양 플랫폼 모두 그룹과 세그먼트 둘 다에 이름을 붙입니다. Flutter 는 트랙 전체를 Semantics(container: true, label: ...)
로 감싸고, Web 루트 <div> 는 같은 값을 aria-label 로 내보냅니다(CouiLocalizations.trackerLabel
— 기본 Tracker, 한국어 트래커; 라벨일 뿐 role 은 아닙니다). 세그먼트마다도 aria-label
이 붙는데, 값은 tooltip ?? status.name — 호출자가 tooltip 을 지정하면 그 텍스트가, 지정하지 않으면 CoreTrackerStatus
의 enum 이름(fine / warning / critical / unknown)이 대신 쓰입니다(Flutter 의
Tooltip(message: tooltip ?? status.name) 과 같은 소스 · 같은 폴백). role 이 없는 <div>
위의 aria-label 이라 그룹/세그먼트 라벨 모두 일부 스크린 리더에서는 무시될 수 있습니다. 실제 role 을 가진 요소는 합성된 Tooltip
패널(role="tooltip") 하나뿐입니다.
키보드#
처리하는 키가 없습니다.
포커스#
포커스를 받는 요소가 하나도 없습니다 — 세그먼트는 비인터랙티브 Container / <div> 입니다. Tab 으로 트래커에 진입할 수 없으므로
hover 로만 열리는 세그먼트 툴팁을 키보드로는 볼 수 없습니다(Web mouseenter / mouseleave, Flutter
Hover → MouseRegion).
스크린 리더#
Flutter 는 로컬라이즈된 "트래커" 한 단어를 말하는 컨테이너 하나로 읽히고 개별 세그먼트 상태는 읽히지 않습니다 — 툴팁 메시지는 hover 시점에 마운트되는 오버레이 안에 있고, 합성된
Tooltip 역시 Semantics(container: true) 만 감쌉니다. Web 은 그룹 이름에 더해 세그먼트마다도 aria-label
이 있지만, role 이 없는 <div> 위의 aria-label 이라 스크린 리더가 실제로 읽어 줄지는 구현체마다 다릅니다.
알려진 제약#
-
tooltip을 지정하지 않은 세그먼트의 접근 가능한 이름은 로컬라이즈되지 않은CoreTrackerStatusenum 이름으로 대체됩니다(Flutter/Web 동일). role="img"/list/meter같은 역할이 없고, 트랙 전체를 대신할 텍스트 요약도 없습니다.- 세그먼트 툴팁에 키보드로 접근할 방법이 없습니다.
상태가 색과 툴팁으로만 구분되므로, 트래커가 전달하는 정보를 화면 밖에서도 알아야 한다면(예: "12개 중 3개 위험") 그 요약 문장을 소비자가 직접 제공해야 합니다.
감속 모션 · 고대비 · 강제 색상 · 최소 터치 타겟처럼 전 컴포넌트에 공통으로 걸리는 축은 전역 접근성 축에서 다룹니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Tracker | Tracker |
| 렌더링 | Row + Expanded + Container |
<div> flex + flex-1 |
| 둥글기 | BorderRadius.horizontal + theme.radiusField |
rounded-l-${radius}
/
rounded-r-${radius}
(
CoreTrackerTokens.endRadius
)
|
| 툴팁 | Tooltip 위젯 | Tooltip 컴포넌트 |
| 상태 → 색상 | CoreTrackerTokens → colorScheme.resolve(...) |
CoreTrackerTokens → bg-${token} Tailwind |