feat(a11y): VoiceOver labels for icon-only buttons + combined action-cell narration

접근성 감사 ③ — 전 화면의 아이콘 전용 버튼과 텍스트 없는 시각
요소에 보이스오버 라벨 부여 (시각 UI 변화 없음):

- 모음 탭: 행동 버튼 셀을 label(이름)+value(측정 중/오늘 누적)로
  한 덩어리 낭독 — 아이콘·타이머가 따로 읽히지 않음. 진행 중 행의
  정지 버튼 "측정 종료".
- 기록 탭: 날짜 이동 화살표(이전/다음 날짜), 내보내기, 기록 필터,
  필터 칩 해제 버튼. 타임테이블의 색 블록·횟수 점은 "행동 이름 +
  시각(구간)" verbatim 라벨로 낭독 가능하게.
- 통계 탭: 기간 이동 화살표(이전/다음 기간), 내보내기, 통계 필터,
  필터 해제.
- 행동/꼬리표/목표 탭: + 추가 버튼(행동/꼬리표/목표 추가), ⋯ 메뉴.
- 일기: 달 이동 화살표(이전/다음 달), 내보내기, ⋯ 메뉴, 기분 타일
  ("오늘 기분"), 할 일 삭제 버튼.
- ⑥ 현재 진행 중 위젯: 종료 버튼 "측정 종료".
- CLAUDE.md §15에 컨벤션 추가: 아이콘 전용 버튼 accessibilityLabel
  필수, 복합 셀은 label+value 한 덩어리, 텍스트 없는 시각 요소는
  verbatim 라벨.

신규 문구 9종 ko/en/ja 번역 + 위젯 카탈로그 '측정 종료' 번역 복사
(전 카탈로그 missing 0/stale 0). Debug·Store 빌드 성공, 접근성
수식어는 시각 렌더링에 영향 없음(스모크 런치 확인). 실기기에서
보이스오버 켜고 핵심 흐름(행동 시작/종료·날짜 이동) 낭독 확인 권장.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-14 22:24:21 +09:00
parent 747aab109d
commit 9c9c24f7df
11 changed files with 201 additions and 0 deletions

View File

@ -330,3 +330,4 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}"
7. 화면과 내보내기가 같은 값을 보여야 하는 곳은 ExportBuilder 스냅숏을 공유할 것 (일기 요약이 그 예)
8. 위젯에서 흰색 하드코딩 금지 — `widgetRenderingMode` 분기 (§10)
9. 시뮬레이터 검증 스크린샷은 `xcrun simctl io <sim> screenshot`, 런치 인자는 콜드 스타트로
10. **아이콘 전용 버튼에는 `accessibilityLabel` 필수** (보이스오버가 "버튼"으로만 읽는 것 방지). 여러 요소로 된 셀은 label+value로 한 덩어리 낭독(모음 탭 행동 셀 참고), 타임테이블 블록·점처럼 텍스트 없는 시각 요소는 verbatim 라벨(이름+시각)로

View File

@ -2863,6 +2863,54 @@
}
}
},
"다음 기간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Next period"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "次の期間"
}
}
}
},
"다음 날짜": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Next day"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "次の日"
}
}
}
},
"다음 달": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Next month"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "次の月"
}
}
}
},
"다이나믹 아일랜드 · 잠금화면": {
"localizations": {
"en": {
@ -3926,6 +3974,22 @@
}
}
},
"메뉴": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Menu"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "メニュー"
}
}
}
},
"메뉴 닫기": {
"localizations": {
"en": {
@ -6766,6 +6830,22 @@
}
}
},
"오늘 %lld회": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%lld times today"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "今日%lld回"
}
}
}
},
"오늘 0분": {
"localizations": {
"en": {
@ -7839,6 +7919,54 @@
}
}
},
"이전 기간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Previous period"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "前の期間"
}
}
}
},
"이전 날짜": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Previous day"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "前の日"
}
}
}
},
"이전 달": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Previous month"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "前の月"
}
}
}
},
"이하 유지": {
"localizations": {
"en": {
@ -10289,6 +10417,22 @@
}
}
},
"필터 해제": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Clear filter"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "フィルターを解除"
}
}
}
},
"필터와 하루 기준": {
"localizations": {
"en": {

View File

@ -92,6 +92,7 @@ struct ActionListView: View {
} label: {
Image(systemName: "ellipsis.circle")
}
.accessibilityLabel(Text("메뉴"))
}
ToolbarItem(placement: .topBarTrailing) {
Button {
@ -103,6 +104,7 @@ struct ActionListView: View {
} label: {
Image(systemName: "plus")
}
.accessibilityLabel(Text("행동 추가"))
}
}
.sheet(isPresented: $showingAdd) {

View File

@ -129,6 +129,7 @@ struct DiaryRootView: View {
} label: {
Image(systemName: "square.and.arrow.up")
}
.accessibilityLabel(Text("일기 내보내기"))
}
}
.sheet(isPresented: $showingExport) {
@ -214,6 +215,7 @@ struct DiaryRootView: View {
.frame(width: 44, height: 36)
.contentShape(Rectangle())
}
.accessibilityLabel(Text("이전 달"))
Spacer()
Text(monthAnchor.formatted(.dateTime.year().month()))
.font(.title3.weight(.bold))
@ -225,6 +227,7 @@ struct DiaryRootView: View {
.frame(width: 44, height: 36)
.contentShape(Rectangle())
}
.accessibilityLabel(Text("다음 달"))
Button("이번 달") {
monthAnchor = math.dayKey(for: .now)
}
@ -603,6 +606,7 @@ struct DiaryDetailView: View {
} label: {
Image(systemName: "ellipsis.circle")
}
.accessibilityLabel(Text("메뉴"))
}
private func pager(_ entry: DiaryEntry) -> some View {
@ -1751,6 +1755,7 @@ private struct DiaryMoodCard: View {
}
.buttonStyle(.plain)
.disabled(readOnly)
.accessibilityLabel(Text("오늘 기분"))
}
private var emojiPicker: some View {
@ -1884,6 +1889,7 @@ private struct DiaryTodoRow: View {
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel(Text("삭제"))
}
}
}

View File

@ -137,6 +137,7 @@ struct GoalListView: View {
} label: {
Image(systemName: "plus")
}
.accessibilityLabel(Text("목표 추가"))
}
}
}

View File

@ -124,6 +124,7 @@ struct HistoryView: View {
} label: {
Image(systemName: "square.and.arrow.up")
}
.accessibilityLabel(Text("내보내기"))
}
ToolbarItem(placement: .topBarTrailing) {
Button {
@ -133,6 +134,7 @@ struct HistoryView: View {
? "line.3.horizontal.decrease.circle.fill"
: "line.3.horizontal.decrease.circle")
}
.accessibilityLabel(Text("기록 필터"))
}
}
.sheet(item: $editingSession) { session in
@ -220,6 +222,7 @@ struct HistoryView: View {
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel(Text("필터 해제"))
}
.foregroundStyle(AppTheme.green)
.padding(.leading, 10)
@ -242,6 +245,7 @@ struct HistoryView: View {
.frame(width: 44, height: 36)
.contentShape(Rectangle())
}
.accessibilityLabel(Text("이전 날짜"))
Spacer()
Button {
showingCalendar = true
@ -283,6 +287,7 @@ struct HistoryView: View {
.frame(width: 44, height: 36)
.contentShape(Rectangle())
}
.accessibilityLabel(Text("다음 날짜"))
Button("오늘") {
selectedDayKey = math.dayKey(for: .now)
}
@ -649,6 +654,9 @@ struct TimetableView: View {
}
}
.buttonStyle(.plain)
//
.accessibilityLabel(Text(verbatim:
"\(item.action.name) \(Format.time(item.range.lowerBound)) ~ \(Format.time(item.range.upperBound))"))
.frame(width: max(width - 4, 8), height: height)
.offset(x: 2, y: y)
}
@ -674,6 +682,8 @@ struct TimetableView: View {
.frame(width: size, height: size)
}
.buttonStyle(.plain)
.accessibilityLabel(Text(verbatim:
"\(item.action.name) +\(item.entry.amount) \(Format.time(item.entry.timestamp))"))
.offset(x: min(x, width - size), y: max(y - size / 2, 0))
}
}

View File

@ -846,6 +846,7 @@ struct RunningSessionRow: View {
.foregroundStyle(AppTheme.yellow)
}
.buttonStyle(.plain)
.accessibilityLabel(Text("측정 종료"))
}
}
}
@ -906,6 +907,19 @@ struct ActionButtonCell: View {
.buttonStyle(PressableButtonStyle())
.sensoryFeedback(.impact(weight: .medium), trigger: bounce)
.disabled(isEditing)
// : " + " (· )
.accessibilityLabel(Text(action.name))
.accessibilityValue(Text(accessibilityValueText))
}
/// ( , )
private var accessibilityValueText: String {
switch action.trackingType {
case .time:
return action.isRunning ? String(localized: "측정 중") : todayLabel
case .count:
return String(localized: "오늘 \(todayCount)")
}
}
@ViewBuilder

View File

@ -87,6 +87,7 @@ struct StatsTabView: View {
} label: {
Image(systemName: "square.and.arrow.up")
}
.accessibilityLabel(Text("내보내기"))
}
ToolbarItem(placement: .topBarTrailing) {
Button {
@ -96,6 +97,7 @@ struct StatsTabView: View {
? "line.3.horizontal.decrease.circle.fill"
: "line.3.horizontal.decrease.circle")
}
.accessibilityLabel(Text("통계 필터"))
}
}
.sheet(isPresented: $showingFilter) {
@ -150,6 +152,7 @@ struct StatsTabView: View {
.frame(width: 44, height: 36)
.contentShape(Rectangle())
}
.accessibilityLabel(Text("이전 기간"))
Spacer()
VStack(spacing: 1) {
Text(periodLabel)
@ -168,6 +171,7 @@ struct StatsTabView: View {
.frame(width: 44, height: 36)
.contentShape(Rectangle())
}
.accessibilityLabel(Text("다음 기간"))
Button(currentPeriodLabel) {
anchorDayKey = math.dayKey(for: .now)
}
@ -247,6 +251,7 @@ struct StatsTabView: View {
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel(Text("필터 해제"))
}
.foregroundStyle(AppTheme.green)
.padding(.leading, 10)

View File

@ -74,6 +74,7 @@ struct TagListView: View {
} label: {
Image(systemName: "plus")
}
.accessibilityLabel(Text("꼬리표 추가"))
}
}
.sheet(item: $editingTag) { tag in

View File

@ -2649,6 +2649,22 @@
}
}
},
"측정 종료": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Stop"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "計測終了"
}
}
}
},
"측정 중": {
"localizations": {
"en": {

View File

@ -216,6 +216,7 @@ struct NowRunningStopButton: View {
.contentShape(.rect)
}
.buttonStyle(.plain)
.accessibilityLabel(Text("측정 종료"))
}
}