Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Projects/Feature/AddWidgetFeature/Sources/AddWidgetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public struct AddWidgetView: View {
VStack(spacing: 0) {
widgetPreview(for: widget)
.padding(.top, 24)
.accessibilityHidden(true)

VStack(spacing: 4) {
Text(widget.kind.title)
Expand All @@ -81,6 +82,8 @@ public struct AddWidgetView: View {
.fill(Color.cardBackground)
}
.padding(.horizontal, 16)
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(widget.kind.title), \(widget.family.title)")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,8 @@ public struct AllergySettingView: View {
.padding(.top, 8)
.padding(.trailing, 16)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(allergy.rawValue)
.accessibilityAddTraits(isAllergyContains ? [.isButton, .isSelected] : .isButton)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import SwiftUI

struct ReviewToast: View {
@Dependency(\.featureFlagClient) private var featureFlagClient
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
let onTap: () -> Void
let onDismiss: () -> Void

var body: some View {
let baseButton = Button {
Expand All @@ -14,6 +16,7 @@ struct ReviewToast: View {
HStack(spacing: 8) {
Image(systemName: "star.bubble.fill")
.foregroundColor(.yellow)
.accessibilityHidden(true)

Text(featureFlagClient.getString(.reviewText) ?? "오늘뭐임을 더 발전시킬 수 있게 리뷰 부탁드려요!")
.font(.system(size: 14, weight: .medium))
Expand All @@ -22,8 +25,11 @@ struct ReviewToast: View {
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
.accessibilityLabel("앱 리뷰 남기기")
.accessibilityHint("탭하면 앱 스토어 리뷰 페이지로 이동합니다")
.accessibilityAction(.escape) { onDismiss() }

if #available(iOS 26.0, *) {
if #available(iOS 26.0, *), !reduceTransparency {
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.

medium

#available(iOS 26.0, *) 확인은 존재하지 않는 iOS 버전을 대상으로 하고 있어 혼란을 줄 수 있습니다. glassEffect는 visionOS에서 사용 가능하므로, #if os(visionOS)와 같은 컴파일러 지시문을 사용하거나, 특정 iOS 기능을 확인하는 것이라면 실제 버전을 사용해야 합니다. 예를 들어 iOS 17.0 이나 visionOS 1.0을 확인하는 것이 더 명확할 것 같습니다. 이 코드가 의도한 바가 무엇인지 확인하고 수정하는 것을 권장합니다.

baseButton
.glassEffect(.regular.interactive(), in: .capsule)
} else {
Expand Down
43 changes: 34 additions & 9 deletions Projects/Feature/MainFeature/Sources/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import NoticeFeature
import SettingsFeature
import SwiftUI
import TimeTableFeature
import UIKit
import TipKit
import TWLog

Expand All @@ -14,6 +15,7 @@ public struct MainView: View {
@ObservedObject var viewStore: ViewStoreOf<MainCore>
@Environment(\.openURL) var openURL
@Environment(\.calendar) var calendar
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Dependency(\.userDefaultsClient) var userDefaultsClient

public init(store: StoreOf<MainCore>) {
Expand All @@ -28,6 +30,7 @@ public struct MainView: View {
.padding(.horizontal, 16)
.padding(.vertical, 12)
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isStaticText)
.accessibilityLabel({
let school: String = viewStore.school
let grade: String = viewStore.grade
Expand All @@ -47,8 +50,6 @@ public struct MainView: View {
items: ["급식", "시간표"]
)
.padding(.top, 32)
.accessibilityLabel("메뉴 탭")
.accessibilityHint("급식과 시간표 중 원하는 메뉴를 선택할 수 있습니다.")

ZStack(alignment: .bottomTrailing) {
TabView(
Expand Down Expand Up @@ -86,18 +87,42 @@ public struct MainView: View {
Color.backgroundSecondary
.ignoresSafeArea()
}
.onChange(of: viewStore.currentTab) { newTab in
let tabName = newTab == 0 ? "급식" : "시간표"
UIAccessibility.post(
notification: .announcement,
argument: "\(tabName) 탭"
)
}

if viewStore.isShowingReviewToast {
ReviewToast {
viewStore.send(.requestReview)
TWLog.event(ClickReviewEventLog())
}
ReviewToast(
onTap: {
viewStore.send(.requestReview)
TWLog.event(ClickReviewEventLog())
},
onDismiss: {
viewStore.send(.hideReviewToast, animation: .default)
}
)
Comment on lines +99 to +107
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

리뷰 토스트 dismiss 경로가 reduceMotion 설정을 완전히 따르지 않습니다.

Line 105(그리고 동일한 Line 127)에서 animation: .default를 고정 사용하면, reduceMotion 활성 시에도 dismiss 애니메이션이 발생할 수 있습니다.

🔧 제안 수정
 ReviewToast(
     onTap: {
         viewStore.send(.requestReview)
         TWLog.event(ClickReviewEventLog())
     },
     onDismiss: {
-        viewStore.send(.hideReviewToast, animation: .default)
+        viewStore.send(.hideReviewToast, animation: reduceMotion ? .none : .default)
     }
 )
 ...
 DispatchQueue.main.asyncAfter(deadline: .now() + 7.5) {
-    viewStore.send(.hideReviewToast, animation: .default)
+    viewStore.send(.hideReviewToast, animation: reduceMotion ? .none : .default)
 }

Also applies to: 111-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Feature/MainFeature/Sources/MainView.swift` around lines 99 - 107,
The dismiss path currently passes a fixed animation (.default) when calling
viewStore.send(.hideReviewToast, animation: .default) from the ReviewToast
usage, which ignores the user's reduce motion setting; change the view to read
the accessibilityReduceMotion environment (e.g.
`@Environment`(\.accessibilityReduceMotion) var reduceMotion) and conditionally
pass animation: reduceMotion ? nil : .default (or use withAnimation only when
reduceMotion is false) for all places where you call
viewStore.send(.hideReviewToast, animation: .default) and similar dismiss/send
calls inside ReviewToast usage so dismiss respects reduce-motion.

.frame(maxWidth: .infinity)
.padding(.horizontal, 16)
.padding(.bottom, 24)
.animation(.default, value: viewStore.isShowingReviewToast)
.transition(.move(edge: .bottom).combined(with: .opacity))
.animation(
reduceMotion ? .none : .default,
value: viewStore.isShowingReviewToast
)
.transition(
reduceMotion
? .opacity
: .move(edge: .bottom).combined(with: .opacity)
)
.onAppear {
UIAccessibility.post(
notification: .announcement,
argument: "앱 리뷰 요청이 표시되었습니다"
)
guard !UIAccessibility.isVoiceOverRunning else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 7.5) {
viewStore.send(.hideReviewToast, animation: .default)
}
Expand Down Expand Up @@ -173,7 +198,7 @@ public struct MainView: View {
}
}
.accessibilityLabel("날짜 선택")
.accessibilityHint("클릭하여 날짜를 선택할 수 있습니다")
.accessibilityRemoveTraits(.isButton)
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

메뉴 트리거에서 버튼 트레이트 제거는 접근성 인지성을 떨어뜨릴 수 있습니다.

Line 201의 .accessibilityRemoveTraits(.isButton)로 인해 상호작용 가능한 컨트롤임을 VoiceOver가 덜 명확히 전달할 수 있습니다. 이 modifier는 제거하는 편이 안전합니다.

🔧 제안 수정
- .accessibilityRemoveTraits(.isButton)
📝 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
.accessibilityRemoveTraits(.isButton)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Feature/MainFeature/Sources/MainView.swift` at line 201, Remove the
.accessibilityRemoveTraits(.isButton) modifier from the menu trigger in MainView
so VoiceOver can correctly announce the control as a button; locate the
occurrence inside the MainView (the menu trigger in the view body) and delete
that modifier (do not replace it with another trait removal), ensuring the
element retains its default .isButton accessibility trait.

}

ToolbarItemGroup(placement: .topBarTrailing) {
Expand Down
16 changes: 15 additions & 1 deletion Projects/Feature/MealFeature/Sources/Weekly/WeeklyMealView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public struct WeeklyMealView: View {
let calText: String = String(format: "%.1f", subMeal.cal)
let calLabel: String = "\(calText) Kcal"
let titleText: String = relativeTitle(for: dayMeal.date, mealType: type)
let accessibilityText: String = "\(titleText) \(calText) 칼로리"
let accessibilityText: String = "\(titleText) \(calText) 칼로리. \(mealTexts.joined(separator: ", "))"
let mealTexts: [String] = subMeal.meals.map { mealDisplay(meal: $0) }
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.

critical

상수 mealTexts가 정의되기 전에 accessibilityText에서 사용되어 컴파일 오류가 발생합니다. mealTexts의 정의를 accessibilityText 정의 앞으로 옮겨주세요.

Suggested change
let accessibilityText: String = "\(titleText) \(calText) 칼로리. \(mealTexts.joined(separator: ", "))"
let mealTexts: [String] = subMeal.meals.map { mealDisplay(meal: $0) }
let mealTexts: [String] = subMeal.meals.map { mealDisplay(meal: $0) }
let accessibilityText: String = "\(titleText) \(calText) 칼로리. \(mealTexts.joined(separator: ", "))"

let dateText: String = "\(dayMeal.date.formatted(.dateTime.month().day().weekday(.wide))) \(type.display)"
let joinedMeals: String = mealTexts.joined(separator: "\n")
Expand Down Expand Up @@ -213,6 +213,20 @@ public struct WeeklyMealView: View {
mealCardView
.accessibilityElement(children: .combine)
.accessibilityLabel(accessibilityText)
.accessibilityAction(named: "텍스트로 복사") {
UIPasteboard.general.string = shareText
TWLog.event(ShareMealEventLog())
}
.accessibilityAction(named: "이미지로 복사") {
if #available(iOS 16.0, *) {
let renderer = ImageRenderer(content: mealCardView)
renderer.scale = displayScale
if let image = renderer.uiImage {
UIPasteboard.general.image = image
TWLog.event(ShareMealImageEventLog())
}
}
}
Comment on lines +220 to +229
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 접근성 액션의 가용성 처리 위치 확인
nl -ba Projects/Feature/MealFeature/Sources/Weekly/WeeklyMealView.swift | sed -n '214,232p'

# 기대 결과:
# - accessibilityAction(named: "이미지로 복사") modifier가 조건 없이 붙어 있음
# - `#available`(iOS 16.0, *) 체크가 액션 클로저 내부에만 존재
# 이 경우 iOS 16 미만에서 no-op 액션 노출 가능성 있음

Repository: todaywhat/TodayWhat-iOS

Length of output: 107


🏁 Script executed:

cat -n Projects/Feature/MealFeature/Sources/Weekly/WeeklyMealView.swift | sed -n '214,232p'

Repository: todaywhat/TodayWhat-iOS

Length of output: 1037


iOS 16 미만에서 "이미지로 복사" 접근성 액션이 노출되지만 실제로 동작하지 않습니다.

Line 220의 .accessibilityAction(named: "이미지로 복사") 수정자는 조건 없이 항상 등록되고, iOS 16 가용성 체크는 액션 클로저 내부(line 221)에만 있습니다. 이로 인해 iOS 16 미만 사용자의 접근성 트리에 no-op 액션이 노출되어 혼란을 초래할 수 있습니다. 액션 등록 자체를 조건부로 분기하여 iOS 16 미만에서는 액션을 노출하지 않는 것이 안전합니다.

제안 수정안
-        mealCardView
-            .accessibilityElement(children: .combine)
-            .accessibilityLabel(accessibilityText)
-            .accessibilityAction(named: "텍스트로 복사") {
-                UIPasteboard.general.string = shareText
-                TWLog.event(ShareMealEventLog())
-            }
-            .accessibilityAction(named: "이미지로 복사") {
-                if `#available`(iOS 16.0, *) {
-                    let renderer = ImageRenderer(content: mealCardView)
-                    renderer.scale = displayScale
-                    if let image = renderer.uiImage {
-                        UIPasteboard.general.image = image
-                        TWLog.event(ShareMealImageEventLog())
-                    }
-                }
-            }
+        let baseCard = mealCardView
+            .accessibilityElement(children: .combine)
+            .accessibilityLabel(accessibilityText)
+            .accessibilityAction(named: "텍스트로 복사") {
+                UIPasteboard.general.string = shareText
+                TWLog.event(ShareMealEventLog())
+            }
+
+        Group {
+            if `#available`(iOS 16.0, *) {
+                baseCard
+                    .accessibilityAction(named: "이미지로 복사") {
+                        let renderer = ImageRenderer(content: mealCardView)
+                        renderer.scale = displayScale
+                        if let image = renderer.uiImage {
+                            UIPasteboard.general.image = image
+                            TWLog.event(ShareMealImageEventLog())
+                        }
+                    }
+            } else {
+                baseCard
+            }
+        }
             .contextMenu {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Feature/MealFeature/Sources/Weekly/WeeklyMealView.swift` around
lines 220 - 229, The accessibility action ".accessibilityAction(named: \"이미지로
복사\")" is always registered but only works on iOS 16+; move the availability
check outwards so the modifier is only applied on iOS 16 and above. Concretely,
wrap the view/modifier application that adds the accessibilityAction in an if
`#available`(iOS 16.0, *) branch (or apply the modifier conditionally using a
Group) so that ImageRenderer(content: mealCardView), renderer.scale =
displayScale, UIPasteboard.general.image assignment and
TWLog.event(ShareMealImageEventLog()) are only reachable when the action is
actually registered. Ensure you reference the existing symbols
accessibilityAction, ImageRenderer, mealCardView, displayScale,
UIPasteboard.general, TWLog and ShareMealImageEventLog when updating the view
modifiers.

.contextMenu {
Button {
UIPasteboard.general.string = shareText
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public struct SchoolSettingView: View {
private let isNavigationPushed: Bool
@FocusState private var focusField: FocusField?
@Environment(\.dismiss) var dismiss
@Environment(\.accessibilityReduceMotion) private var reduceMotion

public init(
store: StoreOf<SchoolSettingCore>,
Expand Down Expand Up @@ -46,9 +47,9 @@ public struct SchoolSettingView: View {
schoolSearchResults(viewStore: viewStore)
}
}
.animation(.default, value: viewStore.grade)
.animation(.default, value: viewStore.class)
.animation(.default, value: viewStore.school)
.animation(reduceMotion ? .none : .default, value: viewStore.grade)
.animation(reduceMotion ? .none : .default, value: viewStore.class)
.animation(reduceMotion ? .none : .default, value: viewStore.school)
Comment on lines +50 to +52
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

감소된 모션 처리가 일부 경로에서만 적용됩니다.

Line 50~52는 잘 반영됐지만, Line 56/71/131/222/245의 animation: .defaultwithAnimation은 여전히 모션을 강제합니다. 접근성 설정 일관성을 위해 동일하게 reduceMotion 분기 적용이 필요합니다.

🔧 제안 수정
 .onChange(of: focusField) { newValue in
-    viewStore.send(.schoolFocusedChanged(newValue == .school), animation: .default)
+    viewStore.send(.schoolFocusedChanged(newValue == .school), animation: reduceMotion ? .none : .default)
 }
 ...
 .onAppear {
     viewStore.send(.onAppear)
-    withAnimation {
+    withAnimation(reduceMotion ? .none : .default) {
         focusField = .school
     }
 }
 ...
 .onTapGesture {
-    viewStore.send(.majorTextFieldDidTap, animation: .default)
+    viewStore.send(.majorTextFieldDidTap, animation: reduceMotion ? .none : .default)
     focusField = nil
 }
 ...
 .onTapGesture {
-    viewStore.send(.schoolRowDidSelect(school), animation: .default)
+    viewStore.send(.schoolRowDidSelect(school), animation: reduceMotion ? .none : .default)
     focusField = .grade
 }
 ...
 TWButton(title: viewStore.nextButtonTitle, style: .wide) {
-    viewStore.send(.nextButtonDidTap, animation: .default)
+    viewStore.send(.nextButtonDidTap, animation: reduceMotion ? .none : .default)
     focusField = nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Feature/SchoolSettingFeature/Sources/SchoolSettingView.swift` around
lines 50 - 52, Several animation calls in SchoolSettingView still force motion;
update every hardcoded .animation(.default, ...) and every withAnimation { ... }
to respect the reduceMotion flag. Replace occurrences of .animation(.default,
value: ...) with .animation(reduceMotion ? .none : .default, value: ...) and
change withAnimation { ... } to withAnimation(reduceMotion ? nil : .default) {
... } (or withAnimation(reduceMotion ? nil : .default) around the exact closure)
for the remaining spots (the other .animation usages and all withAnimation
blocks in SchoolSettingView) so all paths consistently honor reduceMotion.

.padding(.horizontal, 16)
.padding(.top, 24)
.onChange(of: focusField) { newValue in
Expand Down Expand Up @@ -124,14 +125,16 @@ public struct SchoolSettingView: View {
)
)
.disabled(true)
.accessibilityLabel("학과 선택")
.accessibilityHint("학과를 선택하려면 두 번 탭하세요")
}
.padding(.bottom, 16)
.onTapGesture {
viewStore.send(.majorTextFieldDidTap, animation: .default)
focusField = nil
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(viewStore.major.isEmpty ? "학과 선택" : "학과: \(viewStore.major)")
.accessibilityHint("탭하면 학과 선택 시트가 열립니다")
.accessibilityAddTraits(.isButton)
}
}

Expand Down Expand Up @@ -210,7 +213,7 @@ public struct SchoolSettingView: View {

Spacer()
}
.frame(maxWidth: .infinity)
.frame(maxWidth: .infinity, minHeight: 44)
.background {
Color.backgroundMain
}
Expand Down
15 changes: 15 additions & 0 deletions Projects/Feature/SettingsFeature/Sources/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,21 @@ public struct SettingsView: View {
.labelsHidden()
.tint(.textPrimary)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(text)
.accessibilityValue(isOn.wrappedValue ? "활성화됨" : "비활성화됨")
.accessibilityAddTraits(isToggleTraits)
.accessibilityAction {
isOn.wrappedValue.toggle()
}
}

private var isToggleTraits: AccessibilityTraits {
if #available(iOS 17.0, *) {
return .isToggle
} else {
return .isButton
}
}

@ViewBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,22 @@ public struct WeeklyTimeTableCore: Reducer {

public struct WeeklyTimeTable: Equatable {
public let weekdays: [String]
public let fullWeekdays: [String]
public let dates: [String]
public let periods: [Int]
public let subjects: [[String]]
public let todayIndex: Int?

public init(
weekdays: [String],
fullWeekdays: [String],
dates: [String],
periods: [Int],
subjects: [[String]],
todayIndex: Int? = nil
) {
self.weekdays = weekdays
self.fullWeekdays = fullWeekdays
Comment on lines 36 to +45
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

WeeklyTimeTable 배열 길이 불변식을 강제해주세요.

weekdays, fullWeekdays, dates, subjects 길이가 불일치하면 이후 인덱싱에서 런타임 크래시가 발생할 수 있습니다. 공개 이니셜라이저에서 길이 검증을 넣어 불변식을 보장하는 게 안전합니다.

수정 예시
         public init(
             weekdays: [String],
             fullWeekdays: [String],
             dates: [String],
             periods: [Int],
             subjects: [[String]],
             todayIndex: Int? = nil
         ) {
+            precondition(weekdays.count == fullWeekdays.count, "weekdays/fullWeekdays count mismatch")
+            precondition(weekdays.count == dates.count, "weekdays/dates count mismatch")
+            precondition(weekdays.count == subjects.count, "weekdays/subjects count mismatch")
+
             self.weekdays = weekdays
             self.fullWeekdays = fullWeekdays
             self.dates = dates
             self.periods = periods
             self.subjects = subjects
             self.todayIndex = todayIndex
         }
📝 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
public init(
weekdays: [String],
fullWeekdays: [String],
dates: [String],
periods: [Int],
subjects: [[String]],
todayIndex: Int? = nil
) {
self.weekdays = weekdays
self.fullWeekdays = fullWeekdays
public init(
weekdays: [String],
fullWeekdays: [String],
dates: [String],
periods: [Int],
subjects: [[String]],
todayIndex: Int? = nil
) {
precondition(weekdays.count == fullWeekdays.count, "weekdays/fullWeekdays count mismatch")
precondition(weekdays.count == dates.count, "weekdays/dates count mismatch")
precondition(weekdays.count == subjects.count, "weekdays/subjects count mismatch")
self.weekdays = weekdays
self.fullWeekdays = fullWeekdays
self.dates = dates
self.periods = periods
self.subjects = subjects
self.todayIndex = todayIndex
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Feature/TimeTableFeature/Sources/Weekly/WeeklyTimeTableCore.swift`
around lines 36 - 45, Public init in WeeklyTimeTable must enforce array-length
invariants: inside WeeklyTimeTable.init validate that weekdays.count ==
fullWeekdays.count && weekdays.count == dates.count && weekdays.count ==
subjects.count (and if todayIndex != nil assert 0 <= todayIndex! <
weekdays.count); on violation call preconditionFailure (or throw/failable init
per project style) with a clear message so malformed inputs fail early; update
the initializer that assigns self.weekdays, self.fullWeekdays, self.dates,
self.periods, self.subjects, and self.todayIndex to run these checks before
assignment.

self.dates = dates
self.periods = periods
self.subjects = subjects
Expand Down Expand Up @@ -203,6 +206,10 @@ public struct WeeklyTimeTableCore: Reducer {
let weekdays =
showWeekend
? ["월", "화", "수", "목", "금", "토", "일"] : ["월", "화", "수", "목", "금"]
let fullWeekdays =
showWeekend
? ["월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"]
: ["월요일", "화요일", "수요일", "목요일", "금요일"]
var dates: [String] = []

let dateFormatter = DateFormatter()
Expand Down Expand Up @@ -274,6 +281,7 @@ public struct WeeklyTimeTableCore: Reducer {

return WeeklyTimeTable(
weekdays: weekdays,
fullWeekdays: fullWeekdays,
dates: dates,
periods: periods,
subjects: weeklySubjects,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,17 @@ public struct WeeklyTimeTableView: View {
.frame(width: firstColumnWidth, height: headerHeight)

ForEach(0..<weeklyTimeTable.weekdays.count, id: \.self) { index in
let isToday = weeklyTimeTable.isToday(weekdayIndex: index)
VStack(spacing: 2) {
Text(weeklyTimeTable.weekdays[index])
.twFont(horizontalSizeClass == .regular ? .body1 : .body2, color: .textSecondary)
}
.frame(width: cellWidth, height: headerHeight)
.accessibilityLabel(
isToday
? "\(weeklyTimeTable.fullWeekdays[index]), 오늘"
: weeklyTimeTable.fullWeekdays[index]
)
}
}
}
Expand All @@ -247,20 +253,26 @@ public struct WeeklyTimeTableView: View {
Text("\(period)")
.twFont(.body2, color: .textSecondary)
.frame(width: firstColumnWidth, height: cellSide)
.accessibilityLabel("\(period)교시")

ForEach(0..<weeklyTimeTable.weekdays.count, id: \.self) { weekdayIndex in
let subject = weeklyTimeTable.subject(
for: period - 1,
weekday: weekdayIndex
)
let isToday = weeklyTimeTable.isToday(weekdayIndex: weekdayIndex)
let fullWeekdayName = weeklyTimeTable.fullWeekdays[weekdayIndex]
let accessLabel: String = subject.isEmpty
? "\(fullWeekdayName) \(period)교시 수업 없음"
: "\(fullWeekdayName) \(period)교시 \(subject)\(isToday ? ", 오늘" : "")"
Comment on lines +265 to +267
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

빈 셀 접근성 라벨에도 ‘오늘’ 맥락을 일관되게 포함해주세요.

현재는 과목이 있을 때만 , 오늘이 붙고, 수업 없음 케이스에는 오늘 정보가 빠집니다. 오늘 컬럼 탐색 시 음성 피드백 일관성이 깨집니다.

수정 예시
-                let accessLabel: String = subject.isEmpty
-                    ? "\(fullWeekdayName) \(period)교시 수업 없음"
-                    : "\(fullWeekdayName) \(period)교시 \(subject)\(isToday ? ", 오늘" : "")"
+                let todaySuffix = isToday ? ", 오늘" : ""
+                let accessLabel: String = subject.isEmpty
+                    ? "\(fullWeekdayName) \(period)교시 수업 없음\(todaySuffix)"
+                    : "\(fullWeekdayName) \(period)교시 \(subject)\(todaySuffix)"
📝 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
let accessLabel: String = subject.isEmpty
? "\(fullWeekdayName) \(period)교시 수업 없음"
: "\(fullWeekdayName) \(period)교시 \(subject)\(isToday ? ", 오늘" : "")"
let todaySuffix = isToday ? ", 오늘" : ""
let accessLabel: String = subject.isEmpty
? "\(fullWeekdayName) \(period)교시 수업 없음\(todaySuffix)"
: "\(fullWeekdayName) \(period)교시 \(subject)\(todaySuffix)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Feature/TimeTableFeature/Sources/Weekly/WeeklyTimeTableView.swift`
around lines 265 - 267, The accessibility label construction for accessLabel
currently appends ", 오늘" only when subject is non-empty; modify the ternary
branches that build accessLabel (the subject.isEmpty case and the non-empty case
in WeeklyTimeTableView where accessLabel is formed using fullWeekdayName,
period, subject and isToday) so that both branches include the isToday context —
e.g., append the same ", 오늘" suffix when isToday is true even for the
"\(fullWeekdayName) \(period)교시 수업 없음" branch — ensuring consistent vocal
feedback.


Text(subject)
.twFont(
fontForCell(
isToday: weeklyTimeTable.isToday(weekdayIndex: weekdayIndex),
isToday: isToday,
horizontalSizeClass: horizontalSizeClass
),
color: weeklyTimeTable.isToday(weekdayIndex: weekdayIndex) ? Color.extraBlack : .textSecondary
color: isToday ? Color.extraBlack : .textSecondary
)
.multilineTextAlignment(.center)
.minimumScaleFactor(0.3)
Expand All @@ -274,6 +286,7 @@ public struct WeeklyTimeTableView: View {
.frame(height: 1.0)
}
}
.accessibilityLabel(accessLabel)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x8E",
"green" : "0x8E",
"red" : "0x8E"
"blue" : "0x76",
"green" : "0x76",
"red" : "0x76"
}
},
"idiom" : "universal"
Expand All @@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x70",
"green" : "0x5C",
"red" : "0x5C"
"blue" : "0x94",
"green" : "0x94",
"red" : "0x94"
}
},
"idiom" : "universal"
Expand Down
Loading
Loading