From e72451f17196569d2ae4393b0c09d4ee2330e6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 14 Dec 2025 11:59:09 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=B0=BD?= =?UTF-8?q?=EC=97=90=20=EA=B5=AC=EB=8F=85=EA=B4=80=EB=A6=AC,=20=EC=9E=AC?= =?UTF-8?q?=EC=83=9D=EC=84=A4=EC=A0=95,=20=EA=B0=9C=EB=B0=9C=EC=9E=90?= =?UTF-8?q?=EC=97=90=EA=B2=8C=20=EB=AC=B8=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- today-s-sound/Core/Network/Config.swift | 4 +- .../Features/Main/Home/HomeView.swift | 43 +----- .../Presentation/Features/Main/MainView.swift | 13 +- .../NotificationListView.swift | 2 +- .../Settings/ContactDeveloperView.swift | 91 ++++++++++++ .../Settings/PlaybackSettingsView.swift | 140 ++++++++++++++++++ .../Features/Settings/SettingsView.swift | 87 ++++++++++- .../SubscriptionListView.swift | 39 +++-- today-s-sound/Resources/Fonts.swift | 13 +- 9 files changed, 356 insertions(+), 76 deletions(-) create mode 100644 today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift create mode 100644 today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift diff --git a/today-s-sound/Core/Network/Config.swift b/today-s-sound/Core/Network/Config.swift index b476640..e8931d6 100644 --- a/today-s-sound/Core/Network/Config.swift +++ b/today-s-sound/Core/Network/Config.swift @@ -3,8 +3,8 @@ import Foundation enum Config { static var baseURL: String { #if DEBUG - return "https://www.today-sound.com" -// return "http://localhost:8080" // 개발 서버 +// return "https://www.today-sound.com" + return "http://localhost:8080" // 개발 서버 #else return "https://api.todays-sound.com" // 운영 서버 #endif diff --git a/today-s-sound/Presentation/Features/Main/Home/HomeView.swift b/today-s-sound/Presentation/Features/Main/Home/HomeView.swift index 3c60ba6..d986331 100644 --- a/today-s-sound/Presentation/Features/Main/Home/HomeView.swift +++ b/today-s-sound/Presentation/Features/Main/Home/HomeView.swift @@ -12,12 +12,11 @@ struct HomeView: View { .ignoresSafeArea() VStack(spacing: 0) { - Spacer() - // 오늘의 소리 타이틀 Text("오늘의 소리") .font(.KoddiBold56) .foregroundStyle(Color.text(colorScheme)) + .padding(.top, 60) .padding(.bottom, 30) .accessibilityElement() // 이 텍스트를 독립 요소로 .accessibilityLabel("오늘의 소리") // 👉 "오늘의 소리"라고 읽기 @@ -45,42 +44,8 @@ struct HomeView: View { .accessibilityLabel(speechService.isSpeaking ? "재생 중단 버튼" : "재생 시작 버튼") .accessibilityHint(speechService.isSpeaking ? "이중탭하여 재생을 중단합니다" : "이중탭하여 알림을 재생합니다") .padding(.bottom, 60) - - // 속도 조절 - HStack(spacing: 48) { - Button( - action: { viewModel.decreaseRate() }, - label: { - Image(systemName: "minus") - .font(.KoddiBold48) - .foregroundColor(Color.primaryGreen) - } - ) - .accessibilityLabel("재생 속도 감소") - .accessibilityHint("탭하여 재생 속도를 느리게 합니다") - - // 현재 속도 표시 - Text(String(format: "%.1f x", viewModel.playbackRate)) - .font(.KoddiBold48) - .foregroundColor(Color.text(colorScheme)) - .monospacedDigit() - .frame(minWidth: 100) - .accessibilityElement() // 독립 요소 - .accessibilityLabel("현재 속도 \(String(format: "%.1f", viewModel.playbackRate))배속") - // 예: "현재 속도 1.0배속" - - Button( - action: { viewModel.increaseRate() }, - label: { - Image(systemName: "plus") - .font(.KoddiBold48) - .foregroundColor(Color.primaryGreen) - } - ) - .accessibilityLabel("재생 속도 증가") - .accessibilityHint("탭하여 재생 속도를 빠르게 합니다") - } - .padding(.bottom, 60) + + Spacer() VStack(spacing: 16) { // "현재 카테고리" 텍스트 @@ -105,7 +70,7 @@ struct HomeView: View { .accessibilityElement() .accessibilityLabel("피드를 불러오는 중입니다") } else if viewModel.currentCategoryName.isEmpty { - Text("재생할 피드가 없습니다") + Text("등록된 페이지 없음") .font(.KoddiExtraBold32) .foregroundColor(colorScheme == .dark ? .black : .white) .padding(.horizontal, 32) diff --git a/today-s-sound/Presentation/Features/Main/MainView.swift b/today-s-sound/Presentation/Features/Main/MainView.swift index 68be61d..98ce685 100644 --- a/today-s-sound/Presentation/Features/Main/MainView.swift +++ b/today-s-sound/Presentation/Features/Main/MainView.swift @@ -5,7 +5,6 @@ struct MainView: View { case home case feed case notifications - case subscriptions case settings } @@ -43,22 +42,12 @@ struct MainView: View { } .tag(Tab.notifications) - SubscriptionListView() - .tabItem { - VStack { - Image(systemName: "books.vertical.fill") - .accessibilityHidden(true) - Text("구독") - } - } - .tag(Tab.subscriptions) - SettingsView() .tabItem { VStack { Image(systemName: "gearshape.fill") .accessibilityHidden(true) - Text("설정") + Text("관리") } } .tag(Tab.settings) diff --git a/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift b/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift index 533cf3d..e55c060 100644 --- a/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift +++ b/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift @@ -162,7 +162,7 @@ struct NotificationListView: View { AlarmItem( subscriptionId: 3, alias: "장학 공지", - summaryContent: "2025학년도 1학기 장학금 신청 안내입니다. 신청 자격과 필요 서류를 꼭 확인한 뒤 기한 내 제출해주세요.", + summaryContent: "2025학년도 1학기 장학금 신청 안내입니다. 신N청 자격과 필요 서류를 꼭 확인한 뒤 기한 내 제출해주세요.", url: "exurl", timeAgo: "30분 전", isUrgent: true diff --git a/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift b/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift new file mode 100644 index 0000000..3283376 --- /dev/null +++ b/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift @@ -0,0 +1,91 @@ +import SwiftUI + +struct ContactDeveloperView: View { + @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) var dismiss + @State private var emailSubject = "" + @State private var emailBody = "" + + var body: some View { + ZStack { + Color.background(colorScheme) + .ignoresSafeArea() + + VStack(spacing: 0) { + ScreenMainTitle(text: "개발자 문의", colorScheme: colorScheme) + .padding(.top, 16) + + Spacer() + + VStack(spacing: 24) { + + // 문의 안내 텍스트 + VStack(spacing: 12) { + Text("문의사항이 있으신가요?") + .font(.KoddiBold20) + .foregroundColor(Color.text(colorScheme)) + .accessibilityLabel("문의사항이 있으신가요?") + + Text("아래 이메일로 문의해주세요.") + .font(.KoddiRegular16) + .foregroundColor(Color.secondaryText(colorScheme)) + .accessibilityLabel("아래 이메일로 문의해주세요.") + } + + // 이메일 주소 버튼 + Button { + if let url = URL(string: "mailto:support@todayssound.com?subject=문의사항") { + UIApplication.shared.open(url) + } + } label: { + HStack(spacing: 8) { + Image(systemName: "envelope") + .font(.KoddiBold20) + .foregroundColor(Color.primaryGreen) + Text("support@todayssound.com") + .font(.KoddiBold20) + .foregroundColor(Color.primaryGreen) + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 24) + .padding(.vertical, 16) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.primaryGreen, lineWidth: 2) + ) + } + .buttonStyle(PlainButtonStyle()) + .accessibilityLabel("이메일 보내기 버튼") + .accessibilityHint("탭하여 이메일 앱을 엽니다") + } + .padding(.horizontal, 20) + + Spacer() + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.KoddiBold20) + .foregroundColor(Color.text(colorScheme)) + } + .accessibilityLabel("뒤로 가기") + .accessibilityHint("관리 페이지로 돌아갑니다") + } + } + } +} + +struct ContactDeveloperView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + ContactDeveloperView() + } + } +} + diff --git a/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift b/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift new file mode 100644 index 0000000..3d8a22e --- /dev/null +++ b/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift @@ -0,0 +1,140 @@ +import SwiftUI + +struct PlaybackSettingsView: View { + @StateObject private var viewModel = PlaybackSettingsViewModel() + @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) var dismiss + + var body: some View { + ZStack { + Color.background(colorScheme) + .ignoresSafeArea() + + VStack(spacing: 0) { + ScreenMainTitle(text: "재생 설정", colorScheme: colorScheme) + .padding(.top, 16) + + // 재생 속도 설정 + VStack(alignment: .leading, spacing: 16) { + Text("재생 속도 설정") + .font(.KoddiBold20) + .foregroundColor(Color.primaryGreen) + .padding(.horizontal, 20) + .padding(.top, 32) + .accessibilityLabel("재생 속도 설정") + + HStack(spacing: 48) { + Button( + action: { viewModel.decreaseRate() }, + label: { + Image(systemName: "minus") + .font(.KoddiBold48) + .foregroundColor(Color.primaryGreen) + } + ) + .accessibilityLabel("재생 속도 감소") + .accessibilityHint("탭하여 재생 속도를 느리게 합니다") + + Text(String(format: "%.1f x", viewModel.playbackRate)) + .font(.KoddiBold48) + .foregroundColor(Color.text(colorScheme)) + .monospacedDigit() + .frame(minWidth: 100) + .accessibilityElement() + .accessibilityLabel("현재 속도 \(String(format: "%.1f", viewModel.playbackRate))배속") + + Button( + action: { viewModel.increaseRate() }, + label: { + Image(systemName: "plus") + .font(.KoddiBold48) + .foregroundColor(Color.primaryGreen) + } + ) + .accessibilityLabel("재생 속도 증가") + .accessibilityHint("탭하여 재생 속도를 빠르게 합니다") + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 20) + .padding(.bottom, 32) + } + + Divider() + .background(Color.border(colorScheme)) + .padding(.horizontal, 20) + + Spacer() + + // 저장하기 버튼 + Button { + viewModel.saveSettings() + dismiss() + } label: { + Text("저장하기") + .font(.KoddiBold20) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(Color.primaryGreen) + .cornerRadius(8) + } + .padding(.horizontal, 20) + .padding(.bottom, 40) + .accessibilityLabel("저장하기 버튼") + .accessibilityHint("탭하여 설정을 저장합니다") + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.KoddiBold20) + .foregroundColor(Color.text(colorScheme)) + } + .accessibilityLabel("뒤로 가기") + .accessibilityHint("관리 페이지로 돌아갑니다") + } + } + } +} + +// MARK: - ViewModel + +class PlaybackSettingsViewModel: ObservableObject { + @Published var playbackRate: Double = 1.0 + + init() { + // UserDefaults에서 저장된 재생 속도 불러오기 + playbackRate = UserDefaults.standard.double(forKey: "playbackRate") + if playbackRate == 0 { + playbackRate = 1.0 // 기본값 + } + } + + func increaseRate() { + playbackRate = min(2.0, (playbackRate * 10 + 1).rounded() / 10) + } + + func decreaseRate() { + playbackRate = max(0.5, (playbackRate * 10 - 1).rounded() / 10) + } + + func saveSettings() { + UserDefaults.standard.set(playbackRate, forKey: "playbackRate") + // MainViewModel에도 동기화 (필요한 경우) + NotificationCenter.default.post(name: NSNotification.Name("PlaybackRateChanged"), object: nil, userInfo: ["rate": playbackRate]) + } +} + +struct PlaybackSettingsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + PlaybackSettingsView() + } + } +} + diff --git a/today-s-sound/Presentation/Features/Settings/SettingsView.swift b/today-s-sound/Presentation/Features/Settings/SettingsView.swift index 3fcec06..69e8938 100644 --- a/today-s-sound/Presentation/Features/Settings/SettingsView.swift +++ b/today-s-sound/Presentation/Features/Settings/SettingsView.swift @@ -4,6 +4,7 @@ struct SettingsView: View { @EnvironmentObject var session: SessionStore @Environment(\.colorScheme) var colorScheme @State private var showDeleteAlert = false + @AppStorage("highContrastMode") private var highContrastMode = false var body: some View { NavigationView { @@ -12,16 +13,65 @@ struct SettingsView: View { .ignoresSafeArea() VStack(spacing: 0) { - ScreenMainTitle(text: "설정", colorScheme: colorScheme) + ScreenMainTitle(text: "관리", colorScheme: colorScheme) .padding(.top, 16) Spacer() - // 회원탈퇴 버튼 + // 관리 항목 리스트 + VStack(spacing: 0) { + NavigationLink(destination: SubscriptionListView()) { + SettingsRow(title: "구독 관리", colorScheme: colorScheme) + } + .buttonStyle(PlainButtonStyle()) + + Divider() + .background(Color.border(colorScheme)) + .padding(.horizontal, 20) + + NavigationLink(destination: PlaybackSettingsView()) { + SettingsRow(title: "재생 설정", colorScheme: colorScheme) + } + .buttonStyle(PlainButtonStyle()) + + Divider() + .background(Color.border(colorScheme)) + .padding(.horizontal, 20) + + NavigationLink(destination: ContactDeveloperView()) { + SettingsRow(title: "개발자에게 문의", colorScheme: colorScheme) + } + .buttonStyle(PlainButtonStyle()) + + Divider() + .background(Color.border(colorScheme)) + .padding(.horizontal, 20) + + HStack { + Text("고대비 모드 설정") + .font(.KoddiBold24) + .foregroundColor(Color.text(colorScheme)) + Spacer() + Toggle("", isOn: $highContrastMode) + .labelsHidden() + } + .padding(.horizontal, 20) + .padding(.vertical, 16) + .accessibilityElement(children: .combine) + .accessibilityLabel("고대비 모드 설정") + .accessibilityValue(highContrastMode ? "켜짐" : "꺼짐") + .accessibilityHint("탭하여 고대비 모드 설정을 변경합니다") + } + .background(Color.background(colorScheme)) + .cornerRadius(12) + .padding(.horizontal, 20) + .padding(.bottom, 40) + + // 앱 초기화 버튼 Button { showDeleteAlert = true } label: { - Text("회원탈퇴") + Text("앱 초기화") .font(.KoddiBold20) .foregroundColor(.white) .frame(maxWidth: .infinity) @@ -31,14 +81,16 @@ struct SettingsView: View { } .padding(.horizontal, 20) .padding(.bottom, 40) - .accessibilityLabel("회원탈퇴 버튼") - .accessibilityHint("탭하여 회원탈퇴를 진행합니다") + .accessibilityLabel("앱 초기화 버튼") + .accessibilityHint("탭하여 앱을 초기화합니다") + + Spacer() } } .navigationBarHidden(true) - .alert("회원탈퇴", isPresented: $showDeleteAlert) { + .alert("앱 초기화", isPresented: $showDeleteAlert) { Button("취소", role: .cancel) {} - Button("탈퇴하기", role: .destructive) { + Button("초기화하기", role: .destructive) { session.logout() // 앱 종료 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { @@ -46,12 +98,31 @@ struct SettingsView: View { } } } message: { - Text("정말 회원탈퇴를 하시겠습니까?\n모든 데이터가 삭제되고 앱이 종료됩니다.") + Text("정말 앱을 초기화하시겠습니까?\n모든 데이터가 삭제되고 앱이 종료됩니다.") } } } } +// MARK: - Settings Row Component + +struct SettingsRow: View { + let title: String + let colorScheme: ColorScheme + + var body: some View { + HStack { + Text(title) + .font(.KoddiBold24) + .foregroundColor(Color.text(colorScheme)) + Spacer() + } + .padding(.horizontal, 20) + .padding(.vertical, 16) + .contentShape(Rectangle()) + } +} + struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView() diff --git a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift index 94b1e7b..3d2c4fd 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift @@ -3,6 +3,7 @@ import SwiftUI struct SubscriptionListView: View { @StateObject private var viewModel: SubscriptionListViewModel @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) var dismiss @State private var showAddSubscription = false init(viewModel: SubscriptionListViewModel = SubscriptionListViewModel()) { @@ -10,15 +11,14 @@ struct SubscriptionListView: View { } var body: some View { - NavigationView { - ZStack { - Color.background(colorScheme) - .ignoresSafeArea() + ZStack { + Color.background(colorScheme) + .ignoresSafeArea() - VStack(spacing: 12) { - ScreenMainTitle(text: "구독 설정", colorScheme: colorScheme) - ScreenSubTitle(text: "구독 중인 페이지", colorScheme: colorScheme) - .padding(.top, 16) + VStack(spacing: 12) { + ScreenMainTitle(text: "구독 관리", colorScheme: colorScheme) + ScreenSubTitle(text: "구독 중인 페이지", colorScheme: colorScheme) + .padding(.top, 16) // 로딩 상태 if viewModel.isLoading, viewModel.subscriptions.isEmpty { @@ -130,7 +130,21 @@ struct SubscriptionListView: View { .padding(.top, 12) } } - .navigationBarHidden(true) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.KoddiBold20) + .foregroundColor(Color.text(colorScheme)) + } + .accessibilityLabel("뒤로 가기") + .accessibilityHint("관리 페이지로 돌아갑니다") + } + } .onAppear { // 처음 로드 if viewModel.subscriptions.isEmpty, !viewModel.disableAutoLoad { @@ -141,12 +155,11 @@ struct SubscriptionListView: View { // Pull to refresh viewModel.refresh() } - } - .sheet(isPresented: $showAddSubscription) { - AddSubscriptionView() + .sheet(isPresented: $showAddSubscription) { + AddSubscriptionView() + } } } -} struct SubscriptionListView_Previews: PreviewProvider { static var previews: some View { diff --git a/today-s-sound/Resources/Fonts.swift b/today-s-sound/Resources/Fonts.swift index f1e2046..50c7927 100644 --- a/today-s-sound/Resources/Fonts.swift +++ b/today-s-sound/Resources/Fonts.swift @@ -53,7 +53,18 @@ extension Font { static var KoddiBold20: Font { .koddi(type: .bold, size: 20) } - + static var KoddiBold24: Font { + .koddi(type: .bold, size: 24) + } + + static var KoddiBold16: Font { + .koddi(type: .bold, size: 16) + } + + static var KoddiBold18: Font { + .koddi(type: .bold, size: 18) + } + static var KoddiRegular16: Font { .koddi(type: .regular, size: 16) } From 89208e164e82c2e33f5aaf9fe9fb76867e5e0f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 14 Dec 2025 12:19:03 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat=20:=20placeholder=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddSubscription/AddSubscriptionView.swift | 19 ------- .../Component/InputFieldSection.swift | 51 +++++++------------ 2 files changed, 17 insertions(+), 53 deletions(-) diff --git a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift index 9ef1c52..1409512 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift @@ -38,39 +38,20 @@ struct AddSubscriptionView: View { // 1) 웹사이트 URL (필수) InputFieldSection( title: "웹사이트 URL", - placeholder: "https://www.example.com", description: "모니터링할 웹페이지의 정확한 URL을 입력하세요.", isRequired: true, text: $viewModel.urlText, colorScheme: colorScheme ) - // 시각장애인용 안내 - .accessibilityElement(children: .combine) - .accessibilityLabel("웹사이트 URL 입력 칸") - .accessibilityValue( - viewModel.urlText.isEmpty - ? "입력 예시는 https://www.example.com" - : viewModel.urlText - ) - .accessibilityHint("모니터링할 웹페이지의 정확한 주소를 입력하세요.") // 2) 웹페이지 별명 (선택) InputFieldSection( title: "웹페이지 별명", - placeholder: "동국대학교 공지사항", description: "해당 페이지를 식별할 명칭을 입력하세요.", isRequired: false, text: $viewModel.nameText, colorScheme: colorScheme ) - .accessibilityElement(children: .combine) - .accessibilityLabel("웹페이지 별명 입력 칸") - .accessibilityValue( - viewModel.nameText.isEmpty - ? "입력 예시는 동국대학교 공지사항" - : viewModel.nameText - ) - .accessibilityHint("해당 페이지를 구분하기 쉬운 이름을 입력하세요.") // 3) 키워드 필터 VStack(alignment: .leading, spacing: 12) { diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift index af152b5..64bd20c 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift @@ -7,7 +7,6 @@ import SwiftUI struct InputFieldSection: View { let title: String - let placeholder: String let description: String let isRequired: Bool @Binding var text: String @@ -16,7 +15,6 @@ struct InputFieldSection: View { init( title: String, - placeholder: String, description: String, isRequired: Bool = false, text: Binding, @@ -24,7 +22,6 @@ struct InputFieldSection: View { additionalContent: (() -> AnyView)? = nil ) { self.title = title - self.placeholder = placeholder self.description = description self.isRequired = isRequired _text = text @@ -49,33 +46,23 @@ struct InputFieldSection: View { } } - // 커스텀 플레이스홀더가 있는 TextField - ZStack(alignment: .leading) { - if text.isEmpty { - Text(placeholder) - .foregroundColor(Color.secondaryText(colorScheme)) - .padding(.horizontal, 18) - .padding(.vertical, 16) - .font(.KoddiRegular16) - } - - TextField("", text: $text) - .padding(.horizontal, 18) - .padding(.vertical, 16) - .foregroundColor(Color.text(colorScheme)) - .font(.KoddiRegular16) - .accessibilityLabel(title) - .accessibilityHint(description) - .accessibilityValue(text.isEmpty ? placeholder : text) - } - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color.secondaryBackground(colorScheme)) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.border(colorScheme), lineWidth: 1) - ) + // TextField + TextField("", text: $text) + .padding(.horizontal, 18) + .padding(.vertical, 16) + .foregroundColor(Color.text(colorScheme)) + .font(.KoddiRegular16) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.secondaryBackground(colorScheme)) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.border(colorScheme), lineWidth: 1) + ) + .accessibilityLabel("\(title) 편집창") + .accessibilityHint(description) + .accessibilityValue(text.isEmpty ? "" : text) // 추가 컨텐츠 (예: 추천 키워드 배지 등) if let additionalContent { @@ -99,7 +86,6 @@ struct InputFieldSection_Previews: PreviewProvider { VStack(spacing: 24) { InputFieldSection( title: "웹사이트 URL", - placeholder: "https://www.example.com", description: "모니터링할 웹페이지 URL을 입력하세요.", isRequired: true, text: .constant(""), @@ -108,7 +94,6 @@ struct InputFieldSection_Previews: PreviewProvider { InputFieldSection( title: "웹페이지 별명", - placeholder: "동국대학교 공지사항", description: "해당 페이지를 식별할 명칭을 입력하세요. (선택 사항)", isRequired: false, text: .constant("이미 입력된 값"), @@ -122,7 +107,6 @@ struct InputFieldSection_Previews: PreviewProvider { VStack(spacing: 24) { InputFieldSection( title: "웹사이트 URL", - placeholder: "https://www.example.com", description: "모니터링할 웹페이지 URL을 입력하세요.", isRequired: true, text: .constant(""), @@ -131,7 +115,6 @@ struct InputFieldSection_Previews: PreviewProvider { InputFieldSection( title: "웹페이지 별명", - placeholder: "동국대학교 공지사항", description: "해당 페이지를 식별할 명칭을 입력하세요. (선택 사항)", isRequired: false, text: .constant("이미 입력된 값"), From ee354c9fca1dd0b86011384dad34b0575d0c9f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 14 Dec 2025 12:21:36 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EA=B4=80=EB=A6=AC=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD,=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=ED=8F=B0=ED=8A=B8=20=ED=81=AC=EA=B8=B0=20=EC=A4=84=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift | 2 +- today-s-sound/Presentation/Features/Settings/SettingsView.swift | 2 +- .../Features/SubscriptionList/SubscriptionListView.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift b/today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift index 30b1332..2d0e46a 100644 --- a/today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift +++ b/today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift @@ -14,7 +14,7 @@ struct ScreenMainTitle: View { var body: some View { Text(text) - .font(.KoddiBold56) + .font(.KoddiBold48) .foregroundColor(Color.text(colorScheme)) .frame(maxWidth: .infinity, alignment: .center) .multilineTextAlignment(.center) diff --git a/today-s-sound/Presentation/Features/Settings/SettingsView.swift b/today-s-sound/Presentation/Features/Settings/SettingsView.swift index 69e8938..70e585b 100644 --- a/today-s-sound/Presentation/Features/Settings/SettingsView.swift +++ b/today-s-sound/Presentation/Features/Settings/SettingsView.swift @@ -21,7 +21,7 @@ struct SettingsView: View { // 관리 항목 리스트 VStack(spacing: 0) { NavigationLink(destination: SubscriptionListView()) { - SettingsRow(title: "구독 관리", colorScheme: colorScheme) + SettingsRow(title: "구독 페이지 관리", colorScheme: colorScheme) } .buttonStyle(PlainButtonStyle()) diff --git a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift index 3d2c4fd..dc2945e 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift @@ -16,7 +16,7 @@ struct SubscriptionListView: View { .ignoresSafeArea() VStack(spacing: 12) { - ScreenMainTitle(text: "구독 관리", colorScheme: colorScheme) + ScreenMainTitle(text: "구독 페이지 관리", colorScheme: colorScheme) ScreenSubTitle(text: "구독 중인 페이지", colorScheme: colorScheme) .padding(.top, 16) From 134aca177b607bb7a7bf686d8c5cbc354de2213d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 14 Dec 2025 12:46:22 +0900 Subject: [PATCH 4/7] fix : lint --- .../Component/InputFieldSection.swift | 4 + .../Features/Main/Home/HomeView.swift | 2 +- .../Settings/ContactDeveloperView.swift | 2 - .../Settings/PlaybackSettingsView.swift | 1 - .../SubscriptionListView.swift | 236 +++++++++--------- today-s-sound/Resources/Fonts.swift | 25 +- 6 files changed, 136 insertions(+), 134 deletions(-) diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift index 64bd20c..62b3705 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift @@ -48,10 +48,14 @@ struct InputFieldSection: View { // TextField TextField("", text: $text) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(isRequired ? .URL : .default) .padding(.horizontal, 18) .padding(.vertical, 16) .foregroundColor(Color.text(colorScheme)) .font(.KoddiRegular16) + .contentShape(Rectangle()) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.secondaryBackground(colorScheme)) diff --git a/today-s-sound/Presentation/Features/Main/Home/HomeView.swift b/today-s-sound/Presentation/Features/Main/Home/HomeView.swift index d986331..a989498 100644 --- a/today-s-sound/Presentation/Features/Main/Home/HomeView.swift +++ b/today-s-sound/Presentation/Features/Main/Home/HomeView.swift @@ -44,7 +44,7 @@ struct HomeView: View { .accessibilityLabel(speechService.isSpeaking ? "재생 중단 버튼" : "재생 시작 버튼") .accessibilityHint(speechService.isSpeaking ? "이중탭하여 재생을 중단합니다" : "이중탭하여 알림을 재생합니다") .padding(.bottom, 60) - + Spacer() VStack(spacing: 16) { diff --git a/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift b/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift index 3283376..7c7908d 100644 --- a/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift +++ b/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift @@ -18,7 +18,6 @@ struct ContactDeveloperView: View { Spacer() VStack(spacing: 24) { - // 문의 안내 텍스트 VStack(spacing: 12) { Text("문의사항이 있으신가요?") @@ -88,4 +87,3 @@ struct ContactDeveloperView_Previews: PreviewProvider { } } } - diff --git a/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift b/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift index 3d8a22e..e646b33 100644 --- a/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift +++ b/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift @@ -137,4 +137,3 @@ struct PlaybackSettingsView_Previews: PreviewProvider { } } } - diff --git a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift index dc2945e..05892a6 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift @@ -20,146 +20,146 @@ struct SubscriptionListView: View { ScreenSubTitle(text: "구독 중인 페이지", colorScheme: colorScheme) .padding(.top, 16) - // 로딩 상태 - if viewModel.isLoading, viewModel.subscriptions.isEmpty { - Spacer() - ProgressView("불러오는 중...") - .progressViewStyle(CircularProgressViewStyle()) - .accessibilityLabel("구독 목록을 불러오는 중입니다") - .accessibilityHint("잠시만 기다려주세요") - Spacer() - } - - // 에러 메시지 - else if let errorMessage = viewModel.errorMessage { - Spacer() - VStack(spacing: 16) { - Text(errorMessage) - .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) - .accessibilityLabel("오류: \(errorMessage)") - .padding(.bottom) + // 로딩 상태 + if viewModel.isLoading, viewModel.subscriptions.isEmpty { + Spacer() + ProgressView("불러오는 중...") + .progressViewStyle(CircularProgressViewStyle()) + .accessibilityLabel("구독 목록을 불러오는 중입니다") + .accessibilityHint("잠시만 기다려주세요") + Spacer() + } - Button("다시 시도") { - viewModel.refresh() - } - .padding(.horizontal, 24) - .padding(.vertical, 12) + // 에러 메시지 + else if let errorMessage = viewModel.errorMessage { + Spacer() + VStack(spacing: 16) { + Text(errorMessage) .font(.KoddiBold20) - .foregroundColor(Color.white) - .background(Color.primaryGreen) - .cornerRadius(8) - .accessibilityLabel("다시 시도 버튼") - .accessibilityHint("탭하여 구독 목록을 다시 불러옵니다") + .foregroundColor(Color.secondaryText(colorScheme)) + .accessibilityLabel("오류: \(errorMessage)") + .padding(.bottom) + + Button("다시 시도") { + viewModel.refresh() } - Spacer() + .padding(.horizontal, 24) + .padding(.vertical, 12) + .font(.KoddiBold20) + .foregroundColor(Color.white) + .background(Color.primaryGreen) + .cornerRadius(8) + .accessibilityLabel("다시 시도 버튼") + .accessibilityHint("탭하여 구독 목록을 다시 불러옵니다") } - // 빈 상태 - else if viewModel.subscriptions.isEmpty { - Spacer() - VStack(spacing: 16) { - Text("구독 중인 페이지가 없습니다") - .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) - .accessibilityLabel("구독 중인 페이지가 없습니다") - } - Spacer() + Spacer() + } + // 빈 상태 + else if viewModel.subscriptions.isEmpty { + Spacer() + VStack(spacing: 16) { + Text("구독 중인 페이지가 없습니다") + .font(.KoddiBold20) + .foregroundColor(Color.secondaryText(colorScheme)) + .accessibilityLabel("구독 중인 페이지가 없습니다") } + Spacer() + } - // 데이터 있을 때(main) - else { - List { - ForEach(viewModel.subscriptions) { subscription in - SubscriptionCardView( - subscription: subscription, - colorScheme: colorScheme, - onToggleAlarm: { sub in - if sub.isUrgent { - viewModel.blockAlarm(sub) - } else { - viewModel.unblockAlarm(sub) - } - } - ) - .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button(role: .destructive) { - viewModel.deleteSubscription(subscription) - } label: { - Label("삭제", systemImage: "trash") + // 데이터 있을 때(main) + else { + List { + ForEach(viewModel.subscriptions) { subscription in + SubscriptionCardView( + subscription: subscription, + colorScheme: colorScheme, + onToggleAlarm: { sub in + if sub.isUrgent { + viewModel.blockAlarm(sub) + } else { + viewModel.unblockAlarm(sub) } - .accessibilityLabel("구독 삭제") - .accessibilityHint("이 구독을 목록에서 삭제합니다") } - .onAppear { - if let lastIndex = viewModel.subscriptions.indices.last, - let currentIndex = viewModel.subscriptions.firstIndex(where: { $0.id == subscription.id }), - currentIndex >= lastIndex - 4 - { - viewModel.loadMoreIfNeeded(currentItem: subscription) - } + ) + .listRowInsets(EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 16)) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button(role: .destructive) { + viewModel.deleteSubscription(subscription) + } label: { + Label("삭제", systemImage: "trash") } + .accessibilityLabel("구독 삭제") + .accessibilityHint("이 구독을 목록에서 삭제합니다") } - - if viewModel.isLoadingMore { - HStack { - Spacer() - ProgressView() - .padding() - .accessibilityLabel("추가 구독을 불러오는 중입니다") - Spacer() + .onAppear { + if let lastIndex = viewModel.subscriptions.indices.last, + let currentIndex = viewModel.subscriptions.firstIndex(where: { $0.id == subscription.id }), + currentIndex >= lastIndex - 4 + { + viewModel.loadMoreIfNeeded(currentItem: subscription) } - .listRowInsets(EdgeInsets()) - .listRowBackground(Color.clear) } } - .listStyle(.plain) - .scrollContentBackground(.hidden) - } - AddSubscriptionButton( - title: "새 웹페이지 추가", - colorScheme: colorScheme, - isEnabled: true - ) { - showAddSubscription = true + + if viewModel.isLoadingMore { + HStack { + Spacer() + ProgressView() + .padding() + .accessibilityLabel("추가 구독을 불러오는 중입니다") + Spacer() + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + } } - .padding(.horizontal, 20) - .padding(.bottom, 16) - .padding(.top, 12) + .listStyle(.plain) + .scrollContentBackground(.hidden) } - } - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button { - dismiss() - } label: { - Image(systemName: "chevron.left") - .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) - } - .accessibilityLabel("뒤로 가기") - .accessibilityHint("관리 페이지로 돌아갑니다") + AddSubscriptionButton( + title: "새 웹페이지 추가", + colorScheme: colorScheme, + isEnabled: true + ) { + showAddSubscription = true } + .padding(.horizontal, 20) + .padding(.bottom, 16) + .padding(.top, 12) } - .onAppear { - // 처음 로드 - if viewModel.subscriptions.isEmpty, !viewModel.disableAutoLoad { - viewModel.loadSubscriptions() + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + Image(systemName: "chevron.left") + .font(.KoddiBold20) + .foregroundColor(Color.text(colorScheme)) } + .accessibilityLabel("뒤로 가기") + .accessibilityHint("관리 페이지로 돌아갑니다") } - .refreshable { - // Pull to refresh - viewModel.refresh() - } - .sheet(isPresented: $showAddSubscription) { - AddSubscriptionView() + } + .onAppear { + // 처음 로드 + if viewModel.subscriptions.isEmpty, !viewModel.disableAutoLoad { + viewModel.loadSubscriptions() } } + .refreshable { + // Pull to refresh + viewModel.refresh() + } + .sheet(isPresented: $showAddSubscription) { + AddSubscriptionView() + } } +} struct SubscriptionListView_Previews: PreviewProvider { static var previews: some View { diff --git a/today-s-sound/Resources/Fonts.swift b/today-s-sound/Resources/Fonts.swift index 50c7927..456646b 100644 --- a/today-s-sound/Resources/Fonts.swift +++ b/today-s-sound/Resources/Fonts.swift @@ -53,18 +53,19 @@ extension Font { static var KoddiBold20: Font { .koddi(type: .bold, size: 20) } - static var KoddiBold24: Font { - .koddi(type: .bold, size: 24) - } - - static var KoddiBold16: Font { - .koddi(type: .bold, size: 16) - } - - static var KoddiBold18: Font { - .koddi(type: .bold, size: 18) - } - + + static var KoddiBold24: Font { + .koddi(type: .bold, size: 24) + } + + static var KoddiBold16: Font { + .koddi(type: .bold, size: 16) + } + + static var KoddiBold18: Font { + .koddi(type: .bold, size: 18) + } + static var KoddiRegular16: Font { .koddi(type: .regular, size: 16) } From 84fbcb82125e19aa569c622c12a868ba830db0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 14 Dec 2025 13:43:45 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat=20:=20keyword=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?subscription=20post=20api=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Network/Service/APIService.swift | 24 +++ .../Network/Targets/SubscriptionAPI.swift | 14 ++ today-s-sound/Data/Models/Subscription.swift | 16 +- .../AddSubscription/AddSubscriptionView.swift | 67 ++++++- .../AddSubscriptionViewModel.swift | 167 +++++++++++++++--- 5 files changed, 251 insertions(+), 37 deletions(-) diff --git a/today-s-sound/Core/Network/Service/APIService.swift b/today-s-sound/Core/Network/Service/APIService.swift index c108e5d..faca817 100644 --- a/today-s-sound/Core/Network/Service/APIService.swift +++ b/today-s-sound/Core/Network/Service/APIService.swift @@ -9,6 +9,9 @@ protocol APIServiceType { func getSubscriptions( userId: String, deviceSecret: String, page: Int, size: Int ) -> AnyPublisher + func createSubscription( + userId: String, deviceSecret: String, request: CreateSubscriptionRequest + ) -> AnyPublisher func deleteSubscription( userId: String, deviceSecret: String, subscriptionId: Int64 ) -> AnyPublisher @@ -175,6 +178,27 @@ class APIService: APIServiceType { .eraseToAnyPublisher() } + func createSubscription( + userId: String, deviceSecret: String, request: CreateSubscriptionRequest + ) -> AnyPublisher { + subscriptionProvider.requestPublisher(.createSubscription( + userId: userId, + deviceSecret: deviceSecret, + request: request + )) + .mapError { moyaError -> NetworkError in + .requestFailed(moyaError) + } + .flatMap { [weak self] response -> AnyPublisher in + guard let self else { + return Fail(error: NetworkError.unknown) + .eraseToAnyPublisher() + } + return handleResponse(response, decodeTo: CreateSubscriptionResponse.self, debugLabel: "구독 생성 응답") + } + .eraseToAnyPublisher() + } + // MARK: - Subscription API (Delete) func deleteSubscription( diff --git a/today-s-sound/Core/Network/Targets/SubscriptionAPI.swift b/today-s-sound/Core/Network/Targets/SubscriptionAPI.swift index 5893cfe..746a46f 100644 --- a/today-s-sound/Core/Network/Targets/SubscriptionAPI.swift +++ b/today-s-sound/Core/Network/Targets/SubscriptionAPI.swift @@ -10,6 +10,7 @@ import Moya enum SubscriptionAPI { case getSubscriptions(userId: String, deviceSecret: String, page: Int, size: Int) + case createSubscription(userId: String, deviceSecret: String, request: CreateSubscriptionRequest) case deleteSubscription(userId: String, deviceSecret: String, subscriptionId: Int64) case blockAlarm(userId: String, deviceSecret: String, subscriptionId: Int64) case unblockAlarm(userId: String, deviceSecret: String, subscriptionId: Int64) @@ -20,6 +21,8 @@ extension SubscriptionAPI: APITargetType { switch self { case .getSubscriptions: "/api/subscriptions" + case .createSubscription: + "/api/subscriptions" case let .deleteSubscription(_, _, subscriptionId): "/api/subscriptions/\(subscriptionId)" case let .blockAlarm(_, _, subscriptionId): @@ -33,6 +36,8 @@ extension SubscriptionAPI: APITargetType { switch self { case .getSubscriptions: .get + case .createSubscription: + .post case .deleteSubscription: .delete case .blockAlarm, .unblockAlarm: @@ -50,6 +55,8 @@ extension SubscriptionAPI: APITargetType { ], encoding: URLEncoding.queryString ) + case let .createSubscription(_, _, request): + .requestJSONEncodable(request) case .deleteSubscription, .blockAlarm, .unblockAlarm: .requestPlain } @@ -64,6 +71,13 @@ extension SubscriptionAPI: APITargetType { "X-User-ID": userId, "X-Device-Secret": deviceSecret ] + case let .createSubscription(userId, deviceSecret, _): + [ + "Content-Type": "application/json", + "Accept": "application/json", + "X-User-ID": userId, + "X-Device-Secret": deviceSecret + ] case let .deleteSubscription(userId, deviceSecret, _): [ "Content-Type": "application/json", diff --git a/today-s-sound/Data/Models/Subscription.swift b/today-s-sound/Data/Models/Subscription.swift index befe7f2..f13a968 100644 --- a/today-s-sound/Data/Models/Subscription.swift +++ b/today-s-sound/Data/Models/Subscription.swift @@ -6,6 +6,8 @@ import Foundation struct CreateSubscriptionRequest: Codable { let url: String let keywords: [String] + let alias: String? + let isUrgent: Bool } // MARK: - Subscription Response Models @@ -13,11 +15,21 @@ struct CreateSubscriptionRequest: Codable { /// 구독 목록 응답 typealias SubscriptionListResponse = APIResponse<[SubscriptionItem]> -/// 구독 생성 응답 -struct CreateSubscriptionResponse: Codable { +/// 구독 생성 결과 +struct CreateSubscriptionResult: Codable { let subscriptionId: Int64 } +/// 구독 생성 응답 +typealias CreateSubscriptionResponse = APIResponse + +extension CreateSubscriptionResponse { + // 편의 속성: result의 subscriptionId에 직접 접근 + var subscriptionId: Int64 { + result.subscriptionId + } +} + /// 구독 삭제 응답 struct DeleteSubscriptionResponse: Codable { let message: String diff --git a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift index 1409512..e7ae4d6 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift @@ -129,24 +129,37 @@ struct AddSubscriptionView: View { // 하단 고정 "등록 승인 요청" 버튼 AddSubscriptionButton( - title: "등록 승인 요청", + title: viewModel.isLoading ? "등록 중..." : "등록 승인 요청", colorScheme: colorScheme, - isEnabled: viewModel.isSubmitEnabled + isEnabled: viewModel.isSubmitEnabled && !viewModel.isLoading ) { - let payload = viewModel.makeRequestPayload() - // TODO: 나중에 여기서 API 서비스에 payload를 넘겨서 서버로 전송 - print("📤 New Subscription Request:", payload) - dismiss() + viewModel.createSubscription { success in + if success { + dismiss() + } + } } // 접근성: 활성/비활성 상태에 따라 안내 문구 변경 - .accessibilityLabel("등록 승인 요청, 버튼") + .accessibilityLabel(viewModel.isLoading ? "등록 중, 버튼" : "등록 승인 요청, 버튼") .accessibilityHint( - viewModel.isSubmitEnabled + viewModel.isLoading + ? "구독을 등록하는 중입니다" + : viewModel.isSubmitEnabled ? "이 웹사이트 등록 승인을 요청합니다." : "웹사이트 URL을 입력해야 활성화됩니다." ) .padding(.horizontal, 16) .padding(.vertical, 16) + + // 에러 메시지 표시 + if let errorMessage = viewModel.errorMessage { + Text(errorMessage) + .font(.KoddiBold16) + .foregroundColor(.red) + .padding(.horizontal, 16) + .padding(.bottom, 8) + .accessibilityLabel("오류: \(errorMessage)") + } } .frame(maxWidth: .infinity, maxHeight: .infinity) } @@ -155,6 +168,12 @@ struct AddSubscriptionView: View { // 키워드 설정 시트 .sheet(isPresented: $viewModel.showKeywordSelector) { KeywordSelectorSheet(viewModel: viewModel, colorScheme: colorScheme) + .onAppear { + // 키워드 설정 시트가 열릴 때 키워드 목록 로드 + if viewModel.availableKeywords.isEmpty { + viewModel.loadKeywords() + } + } } // 키보드 상단에 항상 "키보드 닫기" 버튼 제공 .toolbar { @@ -207,11 +226,41 @@ struct KeywordSelectorSheet: View { // 스크롤 가능한 키워드 목록 ScrollView { VStack(alignment: .leading, spacing: 16) { - if viewModel.availableKeywords.isEmpty { + if viewModel.isLoadingKeywords { + VStack(spacing: 16) { + ProgressView("불러오는 중...") + .progressViewStyle(CircularProgressViewStyle()) + .accessibilityLabel("키워드 목록을 불러오는 중입니다") + } + .frame(maxWidth: .infinity) + .padding(.vertical, 40) + } else if let errorMessage = viewModel.keywordErrorMessage { + VStack(spacing: 16) { + Text(errorMessage) + .font(.KoddiBold20) + .foregroundColor(Color.secondaryText(colorScheme)) + .accessibilityLabel("오류: \(errorMessage)") + + Button("다시 시도") { + viewModel.loadKeywords() + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .font(.KoddiBold20) + .foregroundColor(.white) + .background(Color.primaryGreen) + .cornerRadius(8) + .accessibilityLabel("다시 시도 버튼") + .accessibilityHint("탭하여 키워드 목록을 다시 불러옵니다") + } + .frame(maxWidth: .infinity) + .padding(.vertical, 40) + } else if viewModel.availableKeywords.isEmpty { VStack(spacing: 16) { Text("등록된 키워드가 없습니다.") .font(.KoddiBold20) .foregroundColor(Color.secondaryText(colorScheme)) + .accessibilityLabel("등록된 키워드가 없습니다") } .frame(maxWidth: .infinity) .padding(.vertical, 40) diff --git a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionViewModel.swift b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionViewModel.swift index a11c40f..50d5697 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionViewModel.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionViewModel.swift @@ -1,3 +1,4 @@ +import Combine import Foundation final class AddSubscriptionViewModel: ObservableObject { @@ -10,45 +11,104 @@ final class AddSubscriptionViewModel: ObservableObject { @Published var selectedKeywords: [String] = [] @Published var showKeywordSelector: Bool = false - // 키워드 목록 (더미 데이터 10개) - @Published var availableKeywords: [String] = [ - "장학금", - "학사공지", - "수업", - "교직", - "학생회", - "봉사활동", - "대회", - "모집공고", - "실습", - "교환학생" - ] + // API 상태 + @Published var isLoading: Bool = false + @Published var errorMessage: String? + @Published var isLoadingKeywords: Bool = false + @Published var keywordErrorMessage: String? + + // 키워드 목록 (API에서 가져옴) + @Published var availableKeywords: [String] = [] + + private let apiService: APIService + private var cancellables = Set() + + init(apiService: APIService = APIService()) { + self.apiService = apiService + } /// URL이 비어있지 않을 때만 전송 가능 var isSubmitEnabled: Bool { !urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - /// 서버로 보낼 요청 DTO (나중에 API 연동 시 그대로 쓰면 됨) - struct NewSubscriptionRequest: Encodable { - let url: String - let name: String? - let isUrgent: Bool - let keywords: [String] - } - /// 현재 입력 상태를 기반으로 Request payload 생성 - func makeRequestPayload() -> NewSubscriptionRequest { - NewSubscriptionRequest( + func makeRequestPayload() -> CreateSubscriptionRequest { + CreateSubscriptionRequest( url: urlText.trimmingCharacters(in: .whitespacesAndNewlines), - name: nameText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + keywords: selectedKeywords, + alias: nameText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : nameText.trimmingCharacters(in: .whitespacesAndNewlines), - isUrgent: isUrgent, - keywords: selectedKeywords + isUrgent: isUrgent ) } + /// 구독 생성 API 호출 + func createSubscription(completion: @escaping (Bool) -> Void) { + guard !isLoading else { return } + + guard let userId = Keychain.getString(for: KeychainKey.userId), + let deviceSecret = Keychain.getString(for: KeychainKey.deviceSecret) + else { + errorMessage = "사용자 정보가 없습니다" + completion(false) + return + } + + isLoading = true + errorMessage = nil + + let request = makeRequestPayload() + + print("📤 구독 생성 요청:", request) + + apiService.createSubscription( + userId: userId, + deviceSecret: deviceSecret, + request: request + ) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { [weak self] apiCompletion in + guard let self else { return } + isLoading = false + + switch apiCompletion { + case .finished: + break + + case let .failure(error): + switch error { + case let .serverError(statusCode): + errorMessage = "서버 오류 (상태: \(statusCode))" + + case .decodingFailed: + errorMessage = "응답 처리 실패" + + case let .requestFailed(requestError): + errorMessage = "요청 실패: \(requestError.localizedDescription)" + + case .invalidURL: + errorMessage = "잘못된 URL" + + case .unknown: + errorMessage = "알 수 없는 오류" + } + + print("❌ 구독 생성 실패: \(errorMessage ?? "")") + completion(false) + } + }, + receiveValue: { [weak self] response in + guard let self else { return } + print("✅ 구독 생성 성공: subscriptionId=\(response.subscriptionId)") + completion(true) + } + ) + .store(in: &cancellables) + } + // MARK: - 키워드 선택 로직 func addKeyword(_ keyword: String) { @@ -69,4 +129,59 @@ final class AddSubscriptionViewModel: ObservableObject { addKeyword(keyword) } } + + // MARK: - 키워드 목록 로드 + + /// 키워드 목록을 API에서 가져오기 + func loadKeywords() { + guard !isLoadingKeywords else { return } + + isLoadingKeywords = true + keywordErrorMessage = nil + + print("📡 키워드 목록 요청") + + apiService.getKeywords() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { [weak self] completion in + guard let self else { return } + isLoadingKeywords = false + + switch completion { + case .finished: + break + + case let .failure(error): + switch error { + case let .serverError(statusCode): + keywordErrorMessage = "서버 오류 (상태: \(statusCode))" + + case .decodingFailed: + keywordErrorMessage = "응답 처리 실패" + + case let .requestFailed(requestError): + keywordErrorMessage = "요청 실패: \(requestError.localizedDescription)" + + case .invalidURL: + keywordErrorMessage = "잘못된 URL" + + case .unknown: + keywordErrorMessage = "알 수 없는 오류" + } + + print("❌ 키워드 목록 조회 실패: \(keywordErrorMessage ?? "")") + } + }, + receiveValue: { [weak self] response in + guard let self else { return } + // KeywordItem 배열에서 name만 추출 + let keywords = response.keywords.map(\.name) + availableKeywords = keywords + + print("✅ 키워드 목록 조회 성공: \(keywords.count)개") + } + ) + .store(in: &cancellables) + } } From 26f7dbd5bfb560d4fac4710b7281a2d1c969ee47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 14 Dec 2025 14:03:25 +0900 Subject: [PATCH 6/7] temp commit --- today-s-sound/App/TodaySSoundApp.swift | 2 + .../Core/AppState/AppThemeManager.swift | 46 ++++++++++++++ .../Component/AddSubscriptionButton.swift | 25 ++++---- .../Base/Component/ScreenMainTitle.swift | 8 +-- .../Base/Component/ScreenSubTitle.swift | 6 +- .../AddSubscription/AddSubscriptionView.swift | 36 +++++------ .../Component/InputFieldSection.swift | 21 +++---- .../Component/KeywordCheckboxRow.swift | 6 +- .../Component/SheetHandler.swift | 8 +-- .../Presentation/Features/Feed/FeedView.swift | 18 +++--- .../Features/Main/Home/HomeView.swift | 16 ++--- .../Presentation/Features/Main/MainView.swift | 60 +++++++++++++++++- .../NotificationList/AlertCardView.swift | 12 ++-- .../NotificationListView.swift | 12 ++-- .../Features/OnBoarding/OnBoardingView.swift | 14 +++-- .../Settings/ContactDeveloperView.swift | 12 ++-- .../Settings/PlaybackSettingsView.swift | 12 ++-- .../Features/Settings/SettingsView.swift | 36 ++++++----- .../Component/StatusBadge.swift | 6 +- .../Component/SubscriptionCardView.swift | 10 +-- .../SubscriptionListView.swift | 18 +++--- today-s-sound/Resources/Colors.swift | 63 +++++++++++++++---- 22 files changed, 298 insertions(+), 149 deletions(-) create mode 100644 today-s-sound/Core/AppState/AppThemeManager.swift diff --git a/today-s-sound/App/TodaySSoundApp.swift b/today-s-sound/App/TodaySSoundApp.swift index 52ec4bd..544de7d 100644 --- a/today-s-sound/App/TodaySSoundApp.swift +++ b/today-s-sound/App/TodaySSoundApp.swift @@ -65,6 +65,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele @main struct TodaySSoundApp: App { @StateObject private var session = SessionStore() + @StateObject private var appTheme = AppThemeManager() @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate @@ -78,6 +79,7 @@ struct TodaySSoundApp: App { } } .environmentObject(session) + .environmentObject(appTheme) } } } diff --git a/today-s-sound/Core/AppState/AppThemeManager.swift b/today-s-sound/Core/AppState/AppThemeManager.swift new file mode 100644 index 0000000..8666beb --- /dev/null +++ b/today-s-sound/Core/AppState/AppThemeManager.swift @@ -0,0 +1,46 @@ +// +// AppThemeManager.swift +// today-s-sound +// +// Created by Assistant +// + +import SwiftUI + +/// 앱 테마 모드 +enum AppTheme: String, CaseIterable { + case normal = "normal" + case highContrast = "highContrast" +} + +/// 앱 테마 관리자 +/// 시스템 다크모드 대신 앱 자체의 고대비/일반 모드를 관리합니다. +@MainActor +final class AppThemeManager: ObservableObject { + @Published var theme: AppTheme { + didSet { + UserDefaults.standard.set(theme.rawValue, forKey: "appTheme") + } + } + + init() { + // UserDefaults에서 저장된 테마 불러오기 + if let savedTheme = UserDefaults.standard.string(forKey: "appTheme"), + let theme = AppTheme(rawValue: savedTheme) { + self.theme = theme + } else { + // 기본값은 일반 모드 + self.theme = .normal + } + } + + /// 고대비 모드 활성화 여부 + var isHighContrast: Bool { + theme == .highContrast + } + + /// 테마 토글 + func toggleTheme() { + theme = theme == .normal ? .highContrast : .normal + } +} diff --git a/today-s-sound/Presentation/Base/Component/AddSubscriptionButton.swift b/today-s-sound/Presentation/Base/Component/AddSubscriptionButton.swift index fe3f73c..24da3d5 100644 --- a/today-s-sound/Presentation/Base/Component/AddSubscriptionButton.swift +++ b/today-s-sound/Presentation/Base/Component/AddSubscriptionButton.swift @@ -11,8 +11,8 @@ struct AddSubscriptionButton: View { /// 버튼에 표시할 텍스트 (예: "등록 승인 요청", "저장하기") let title: String - /// 컬러 스킴 (라이트/다크에 따라 글자색만 바뀜) - let colorScheme: ColorScheme + /// 앱 테마 (고대비/일반 모드에 따라 글자색만 바뀜) + let colorScheme: AppTheme /// 버튼 활성/비활성 여부 let isEnabled: Bool @@ -21,8 +21,8 @@ struct AddSubscriptionButton: View { let action: () -> Void private var textColor: Color { - // 배경색은 그대로 두고, 글자색만 모드에 따라 변경 - colorScheme == .dark ? .black : .white + // 배경색은 그대로 두고, 글자색은 항상 흰색 + .white } var body: some View { @@ -52,30 +52,29 @@ struct AddSubscriptionButton: View { struct AddSubscriptionButton_Previews: PreviewProvider { static var previews: some View { Group { - // Light Mode + // Normal Mode AddSubscriptionButton( title: "등록 승인 요청", - colorScheme: .light, + colorScheme: .normal, isEnabled: true, action: {} ) - .previewDisplayName("Light Mode") + .previewDisplayName("Normal Mode") .previewLayout(.sizeThatFits) .padding() - .background(Color.background(.light)) + .background(Color.background(.normal)) - // Dark Mode (배경색은 동일, 글자색만 검정) + // High Contrast Mode AddSubscriptionButton( title: "등록 승인 요청", - colorScheme: .dark, + colorScheme: .highContrast, isEnabled: true, action: {} ) - .previewDisplayName("Dark Mode") + .previewDisplayName("High Contrast Mode") .previewLayout(.sizeThatFits) .padding() - .background(Color.background(.light)) - .preferredColorScheme(.dark) + .background(Color.background(.highContrast)) } } } diff --git a/today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift b/today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift index 2d0e46a..00c0ee0 100644 --- a/today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift +++ b/today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift @@ -10,12 +10,12 @@ import SwiftUI /// 공통 타이틀 컴포넌트. 다양한 화면에서 재사용 가능 struct ScreenMainTitle: View { let text: String - let colorScheme: ColorScheme + let theme: AppTheme var body: some View { Text(text) .font(.KoddiBold48) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(theme)) .frame(maxWidth: .infinity, alignment: .center) .multilineTextAlignment(.center) .padding(.horizontal, 24) @@ -28,8 +28,8 @@ struct ScreenMainTitle: View { struct ScreenSectionTitle_Previews: PreviewProvider { static var previews: some View { VStack(spacing: 16) { - ScreenMainTitle(text: "최근 알림", colorScheme: .light) - ScreenMainTitle(text: "구독 설정", colorScheme: .light) + ScreenMainTitle(text: "최근 알림", theme: .normal) + ScreenMainTitle(text: "구독 설정", theme: .normal) } .padding() } diff --git a/today-s-sound/Presentation/Base/Component/ScreenSubTitle.swift b/today-s-sound/Presentation/Base/Component/ScreenSubTitle.swift index ce1d97d..7b5ac6b 100644 --- a/today-s-sound/Presentation/Base/Component/ScreenSubTitle.swift +++ b/today-s-sound/Presentation/Base/Component/ScreenSubTitle.swift @@ -9,7 +9,7 @@ import SwiftUI struct ScreenSubTitle: View { let text: String - let colorScheme: ColorScheme + let theme: AppTheme var body: some View { Text(text) @@ -26,8 +26,8 @@ struct ScreenSubTitle: View { struct ScreenTitle_Previews: PreviewProvider { static var previews: some View { VStack(spacing: 0) { - ScreenSubTitle(text: "새 웹페이지 추가", colorScheme: .light) - ScreenSubTitle(text: "새 웹페이지 추가", colorScheme: .dark) + ScreenSubTitle(text: "새 웹페이지 추가", theme: .normal) + ScreenSubTitle(text: "새 웹페이지 추가", theme: .highContrast) } } } diff --git a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift index e7ae4d6..d43f640 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift @@ -2,12 +2,12 @@ import SwiftUI struct AddSubscriptionView: View { @StateObject private var viewModel = AddSubscriptionViewModel() - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appTheme: AppThemeManager @Environment(\.dismiss) var dismiss var body: some View { ZStack { - Color.background(colorScheme) + Color.background(appTheme.theme) .ignoresSafeArea() .onTapGesture { // 배경 탭하면 키보드만 닫기 @@ -16,7 +16,7 @@ struct AddSubscriptionView: View { VStack(spacing: 0) { // 상단 핸들 바 (X 대신) - SheetHandleBar(colorScheme: colorScheme) + SheetHandleBar(colorScheme: appTheme.theme) .accessibilityElement() .accessibilityLabel("새 웹페이지 추가 창 닫기") .accessibilityHint("이 영역을 두 번 탭하거나 아래로 스와이프하면 창이 닫힙니다.") @@ -26,7 +26,7 @@ struct AddSubscriptionView: View { } // 화면 제목 - ScreenSubTitle(text: "새 웹페이지 추가", colorScheme: colorScheme) + ScreenSubTitle(text: "새 웹페이지 추가", theme: appTheme.theme) .padding(.bottom, 8) .padding(.top, 4) @@ -41,7 +41,7 @@ struct AddSubscriptionView: View { description: "모니터링할 웹페이지의 정확한 URL을 입력하세요.", isRequired: true, text: $viewModel.urlText, - colorScheme: colorScheme + colorScheme: appTheme.theme ) // 2) 웹페이지 별명 (선택) @@ -50,7 +50,7 @@ struct AddSubscriptionView: View { description: "해당 페이지를 식별할 명칭을 입력하세요.", isRequired: false, text: $viewModel.nameText, - colorScheme: colorScheme + colorScheme: appTheme.theme ) // 3) 키워드 필터 @@ -58,7 +58,7 @@ struct AddSubscriptionView: View { VStack(alignment: .leading, spacing: 8) { Text("키워드 필터") .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) Button(action: { viewModel.showKeywordSelector = true @@ -66,18 +66,18 @@ struct AddSubscriptionView: View { HStack { Text(viewModel.selectedKeywords.isEmpty ? "키워드 추가..." : "키워드 수정...") .font(.KoddiRegular16) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) Spacer() } .padding(.horizontal, 18) .padding(.vertical, 16) .background( RoundedRectangle(cornerRadius: 8) - .fill(Color.secondaryBackground(colorScheme)) + .fill(Color.secondaryBackground(appTheme.theme)) ) .overlay( RoundedRectangle(cornerRadius: 8) - .stroke(Color.border(colorScheme), lineWidth: 1) + .stroke(Color.border(appTheme.theme), lineWidth: 1) ) } .accessibilityLabel(viewModel.selectedKeywords.isEmpty ? "키워드 추가 버튼" : "키워드 수정 버튼") @@ -85,7 +85,7 @@ struct AddSubscriptionView: View { Text("관심 키워드가 포함된 글을 알림으로 받아보세요.") .font(.KoddiRegular16) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) .fixedSize(horizontal: false, vertical: true) .accessibilityLabel("관심 키워드가 포함된 글을 알림으로 받아보세요") } @@ -96,7 +96,7 @@ struct AddSubscriptionView: View { ForEach(viewModel.selectedKeywords, id: \.self) { keyword in KeywordBadgeWithDelete( text: keyword, - colorScheme: colorScheme + colorScheme: appTheme.theme ) { viewModel.removeKeyword(keyword) } @@ -109,7 +109,7 @@ struct AddSubscriptionView: View { HStack { Text("긴급 알림으로 설정") .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) .accessibilityLabel("긴급 알림으로 설정") Spacer() Toggle("", isOn: $viewModel.isUrgent) @@ -130,7 +130,7 @@ struct AddSubscriptionView: View { // 하단 고정 "등록 승인 요청" 버튼 AddSubscriptionButton( title: viewModel.isLoading ? "등록 중..." : "등록 승인 요청", - colorScheme: colorScheme, + colorScheme: appTheme.theme, isEnabled: viewModel.isSubmitEnabled && !viewModel.isLoading ) { viewModel.createSubscription { success in @@ -167,7 +167,7 @@ struct AddSubscriptionView: View { .ignoresSafeArea(.keyboard, edges: .bottom) // 키워드 설정 시트 .sheet(isPresented: $viewModel.showKeywordSelector) { - KeywordSelectorSheet(viewModel: viewModel, colorScheme: colorScheme) + KeywordSelectorSheet(viewModel: viewModel, colorScheme: appTheme.theme) .onAppear { // 키워드 설정 시트가 열릴 때 키워드 목록 로드 if viewModel.availableKeywords.isEmpty { @@ -198,7 +198,7 @@ struct AddSubscriptionView: View { struct KeywordSelectorSheet: View { @ObservedObject var viewModel: AddSubscriptionViewModel - let colorScheme: ColorScheme + let colorScheme: AppTheme @Environment(\.dismiss) var dismiss var body: some View { @@ -220,7 +220,7 @@ struct KeywordSelectorSheet: View { } // 키워드 설정 화면 제목 - ScreenSubTitle(text: "키워드 설정", colorScheme: colorScheme) + ScreenSubTitle(text: "키워드 설정", theme: colorScheme) VStack(spacing: 0) { // 스크롤 가능한 키워드 목록 @@ -312,7 +312,7 @@ struct KeywordSelectorSheet: View { /// 삭제 버튼이 있는 키워드 배지 struct KeywordBadgeWithDelete: View { let text: String - let colorScheme: ColorScheme + let colorScheme: AppTheme let onDelete: () -> Void var body: some View { diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift index 62b3705..be014dd 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift @@ -10,7 +10,7 @@ struct InputFieldSection: View { let description: String let isRequired: Bool @Binding var text: String - let colorScheme: ColorScheme + let colorScheme: AppTheme let additionalContent: (() -> AnyView)? init( @@ -18,7 +18,7 @@ struct InputFieldSection: View { description: String, isRequired: Bool = false, text: Binding, - colorScheme: ColorScheme, + colorScheme: AppTheme, additionalContent: (() -> AnyView)? = nil ) { self.title = title @@ -93,7 +93,7 @@ struct InputFieldSection_Previews: PreviewProvider { description: "모니터링할 웹페이지 URL을 입력하세요.", isRequired: true, text: .constant(""), - colorScheme: .light + colorScheme: .normal ) InputFieldSection( @@ -101,12 +101,12 @@ struct InputFieldSection_Previews: PreviewProvider { description: "해당 페이지를 식별할 명칭을 입력하세요. (선택 사항)", isRequired: false, text: .constant("이미 입력된 값"), - colorScheme: .light + colorScheme: .normal ) } .padding() - .background(Color.background(.light)) - .previewDisplayName("Light Mode") + .background(Color.background(.normal)) + .previewDisplayName("Normal Mode") VStack(spacing: 24) { InputFieldSection( @@ -114,7 +114,7 @@ struct InputFieldSection_Previews: PreviewProvider { description: "모니터링할 웹페이지 URL을 입력하세요.", isRequired: true, text: .constant(""), - colorScheme: .dark + colorScheme: .highContrast ) InputFieldSection( @@ -122,13 +122,12 @@ struct InputFieldSection_Previews: PreviewProvider { description: "해당 페이지를 식별할 명칭을 입력하세요. (선택 사항)", isRequired: false, text: .constant("이미 입력된 값"), - colorScheme: .dark + colorScheme: .highContrast ) } .padding() - .background(Color.background(.dark)) - .preferredColorScheme(.dark) - .previewDisplayName("Dark Mode") + .background(Color.background(.highContrast)) + .previewDisplayName("High Contrast Mode") } } } diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift index 31f84c1..c22aeac 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift @@ -9,7 +9,7 @@ import SwiftUI struct KeywordCheckboxRow: View { let keyword: String let isSelected: Bool - let colorScheme: ColorScheme + let colorScheme: AppTheme let action: () -> Void var body: some View { @@ -54,14 +54,14 @@ struct KeywordCheckboxRow_Previews: PreviewProvider { KeywordCheckboxRow( keyword: "시각장애", isSelected: true, - colorScheme: .light, + colorScheme: .normal, action: {} ) KeywordCheckboxRow( keyword: "접근성", isSelected: false, - colorScheme: .dark, + colorScheme: .highContrast, action: {} ) } diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/SheetHandler.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/SheetHandler.swift index 72cfc64..00438ae 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/Component/SheetHandler.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/SheetHandler.swift @@ -9,7 +9,7 @@ import SwiftUI /// 시트 상단에 보이는 작은 핸들 바 struct SheetHandleBar: View { - let colorScheme: ColorScheme + let colorScheme: AppTheme var body: some View { VStack(spacing: 8) { @@ -29,9 +29,9 @@ struct SheetHandleBar: View { struct SheetHandleBar_Previews: PreviewProvider { static var previews: some View { VStack { - SheetHandleBar(colorScheme: .light) - SheetHandleBar(colorScheme: .dark) + SheetHandleBar(colorScheme: .normal) + SheetHandleBar(colorScheme: .highContrast) } - .background(Color.background(.light)) + .background(Color.background(.normal)) } } diff --git a/today-s-sound/Presentation/Features/Feed/FeedView.swift b/today-s-sound/Presentation/Features/Feed/FeedView.swift index 9c2be33..af6f84d 100644 --- a/today-s-sound/Presentation/Features/Feed/FeedView.swift +++ b/today-s-sound/Presentation/Features/Feed/FeedView.swift @@ -2,7 +2,7 @@ import SwiftUI struct FeedView: View { @StateObject private var viewModel = FeedViewModel() - @Environment(\.colorScheme) private var colorScheme + @EnvironmentObject var appTheme: AppThemeManager /// 현재 선택된 필터 (기본값: "전체") @State private var selectedFilter: String = "전체" @@ -26,7 +26,7 @@ struct FeedView: View { var body: some View { NavigationView { ZStack { - Color.background(colorScheme) + Color.background(appTheme.theme) .ignoresSafeArea() content @@ -73,7 +73,7 @@ struct FeedView: View { Spacer() Text(message) .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) .multilineTextAlignment(.center) .padding(.horizontal, 24) @@ -98,7 +98,7 @@ struct FeedView: View { Spacer() Text("표시할 피드가 없습니다") .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) .multilineTextAlignment(.center) .accessibilityLabel("표시할 피드가 없습니다") Spacer() @@ -119,7 +119,7 @@ struct FeedView: View { // 필터된 카드 리스트 ForEach(filteredItems) { item in - FeedCard(item: item, colorScheme: colorScheme) + FeedCard(item: item, colorScheme: appTheme.theme) .padding(.horizontal, 16) .onAppear { viewModel.loadMoreIfNeeded(currentItem: item) @@ -167,20 +167,20 @@ struct FeedView: View { .fill( isSelected ? Color.primaryGreen - : Color.secondaryBackground(colorScheme) + : Color.secondaryBackground(appTheme.theme) ) ) .foregroundColor( isSelected ? Color.white - : Color.text(colorScheme) + : Color.text(appTheme.theme) ) .overlay( Capsule() .stroke( isSelected ? Color.primaryGreen - : Color.border(colorScheme), + : Color.border(appTheme.theme), lineWidth: 1 ) ) @@ -199,7 +199,7 @@ struct FeedView: View { private struct FeedCard: View { let item: FeedItem - let colorScheme: ColorScheme + let colorScheme: AppTheme var body: some View { VStack(alignment: .leading, spacing: 12) { diff --git a/today-s-sound/Presentation/Features/Main/Home/HomeView.swift b/today-s-sound/Presentation/Features/Main/Home/HomeView.swift index a989498..8fd8285 100644 --- a/today-s-sound/Presentation/Features/Main/Home/HomeView.swift +++ b/today-s-sound/Presentation/Features/Main/Home/HomeView.swift @@ -3,19 +3,19 @@ import SwiftUI struct HomeView: View { @StateObject private var viewModel = MainViewModel() @ObservedObject private var speechService = SpeechService.shared - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appTheme: AppThemeManager var body: some View { ZStack { - // 다크모드에 따라 배경색 변경 - Color.background(colorScheme) + // 앱 테마에 따라 배경색 변경 + Color.background(appTheme.theme) .ignoresSafeArea() VStack(spacing: 0) { // 오늘의 소리 타이틀 Text("오늘의 소리") .font(.KoddiBold56) - .foregroundStyle(Color.text(colorScheme)) + .foregroundStyle(Color.text(appTheme.theme)) .padding(.top, 60) .padding(.bottom, 30) .accessibilityElement() // 이 텍스트를 독립 요소로 @@ -51,14 +51,14 @@ struct HomeView: View { // "현재 카테고리" 텍스트 Text("현재 카테고리") .font(.KoddiBold28) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) .accessibilityElement() .accessibilityLabel("현재 카테고리") if viewModel.isLoading { Text("불러오는 중...") .font(.KoddiExtraBold32) - .foregroundColor(colorScheme == .dark ? .black : .white) + .foregroundColor(.white) .padding(.horizontal, 32) .padding(.vertical, 18) .frame(width: 360, height: 84) @@ -72,7 +72,7 @@ struct HomeView: View { } else if viewModel.currentCategoryName.isEmpty { Text("등록된 페이지 없음") .font(.KoddiExtraBold32) - .foregroundColor(colorScheme == .dark ? .black : .white) + .foregroundColor(.white) .padding(.horizontal, 32) .padding(.vertical, 18) .frame(width: 360, height: 84) @@ -87,7 +87,7 @@ struct HomeView: View { // 현재 카테고리 이름 카드 Text(viewModel.currentCategoryName) .font(.KoddiExtraBold32) - .foregroundColor(colorScheme == .dark ? .black : .white) + .foregroundColor(.white) .padding(.horizontal, 32) .padding(.vertical, 18) .frame(width: 360, height: 84) diff --git a/today-s-sound/Presentation/Features/Main/MainView.swift b/today-s-sound/Presentation/Features/Main/MainView.swift index 98ce685..3141add 100644 --- a/today-s-sound/Presentation/Features/Main/MainView.swift +++ b/today-s-sound/Presentation/Features/Main/MainView.swift @@ -1,4 +1,5 @@ import SwiftUI +import UIKit struct MainView: View { private enum Tab: Hashable { @@ -9,6 +10,7 @@ struct MainView: View { } @State private var selectedTab: Tab = .home + @EnvironmentObject var appTheme: AppThemeManager var body: some View { TabView(selection: $selectedTab) { @@ -52,7 +54,63 @@ struct MainView: View { } .tag(Tab.settings) } - .tint(.primaryGreen) + .tint(appTheme.isHighContrast ? .white : .primaryGreen) + .onAppear { + setupTabBarAppearance() + } + .onChange(of: appTheme.theme) { _ in + setupTabBarAppearance() + } + .onChange(of: selectedTab) { _ in + // 탭이 변경될 때마다 TabBar appearance 재설정 + DispatchQueue.main.async { + setupTabBarAppearance() + } + } + } + + private func setupTabBarAppearance() { + let appearance = UITabBarAppearance() + + // TabBar 배경색을 앱 테마에 맞게 설정 + if appTheme.isHighContrast { + // 고대비 모드: 검은 배경 + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = .black + + // 선택되지 않은 탭 아이템 색상 + appearance.stackedLayoutAppearance.normal.iconColor = .white.withAlphaComponent(0.6) + appearance.stackedLayoutAppearance.normal.titleTextAttributes = [ + .foregroundColor: UIColor.white.withAlphaComponent(0.6) + ] + + // 선택된 탭 아이템 색상 + appearance.stackedLayoutAppearance.selected.iconColor = .white + appearance.stackedLayoutAppearance.selected.titleTextAttributes = [ + .foregroundColor: UIColor.white + ] + } else { + // 일반 모드: 흰 배경 + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = .white + + // 선택되지 않은 탭 아이템 색상 + appearance.stackedLayoutAppearance.normal.iconColor = .black.withAlphaComponent(0.6) + appearance.stackedLayoutAppearance.normal.titleTextAttributes = [ + .foregroundColor: UIColor.black.withAlphaComponent(0.6) + ] + + // 선택된 탭 아이템 색상 + appearance.stackedLayoutAppearance.selected.iconColor = UIColor(Color.primaryGreen) + appearance.stackedLayoutAppearance.selected.titleTextAttributes = [ + .foregroundColor: UIColor(Color.primaryGreen) + ] + } + + UITabBar.appearance().standardAppearance = appearance + if #available(iOS 15.0, *) { + UITabBar.appearance().scrollEdgeAppearance = appearance + } } } diff --git a/today-s-sound/Presentation/Features/NotificationList/AlertCardView.swift b/today-s-sound/Presentation/Features/NotificationList/AlertCardView.swift index 1d77263..89a58f2 100644 --- a/today-s-sound/Presentation/Features/NotificationList/AlertCardView.swift +++ b/today-s-sound/Presentation/Features/NotificationList/AlertCardView.swift @@ -7,14 +7,14 @@ import SwiftUI struct AlertCardView: View { let alarm: AlarmItem - let colorScheme: ColorScheme + let colorScheme: AppTheme private var cardColor: Color { alarm.isUrgent ? .urgentPink : .primaryGreen } private var textColor: Color { - colorScheme == .dark ? .black : .white + .white } var body: some View { @@ -82,16 +82,16 @@ struct AlertCardView_Previews: PreviewProvider { static var previews: some View { Group { ForEach(sampleAlarms) { alarm in - AlertCardView(alarm: alarm, colorScheme: .light) + AlertCardView(alarm: alarm, colorScheme: .normal) .padding() - .previewDisplayName("Card Light - \(alarm.alias)") + .previewDisplayName("Card Normal - \(alarm.alias)") } ForEach(sampleAlarms) { alarm in - AlertCardView(alarm: alarm, colorScheme: .dark) + AlertCardView(alarm: alarm, colorScheme: .highContrast) .padding() .background(Color.black) - .previewDisplayName("Card Dark - \(alarm.alias)") + .previewDisplayName("Card High Contrast - \(alarm.alias)") } } .previewLayout(.sizeThatFits) diff --git a/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift b/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift index e55c060..0cd72a9 100644 --- a/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift +++ b/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift @@ -2,7 +2,7 @@ import SwiftUI struct NotificationListView: View { @StateObject private var viewModel: NotificationListViewModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appTheme: AppThemeManager init(viewModel: NotificationListViewModel = NotificationListViewModel()) { _viewModel = StateObject(wrappedValue: viewModel) @@ -11,11 +11,11 @@ struct NotificationListView: View { var body: some View { NavigationView { ZStack { - Color.background(colorScheme) + Color.background(appTheme.theme) .ignoresSafeArea() VStack(alignment: .leading, spacing: 4) { - ScreenMainTitle(text: "최근 알림", colorScheme: colorScheme) + ScreenMainTitle(text: "최근 알림", theme: appTheme.theme) .padding(.horizontal, 20) content @@ -46,7 +46,7 @@ struct NotificationListView: View { VStack(spacing: 16) { Text(errorMessage) .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) .accessibilityLabel("오류: \(errorMessage)") .padding(.bottom) @@ -70,7 +70,7 @@ struct NotificationListView: View { VStack(spacing: 16) { Text("새로운 알림이 없습니다") .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) .accessibilityLabel("새로운 알림이 없습니다") } Spacer() @@ -79,7 +79,7 @@ struct NotificationListView: View { else { List { ForEach(viewModel.alarms) { alarm in - AlertCardView(alarm: alarm, colorScheme: colorScheme) + AlertCardView(alarm: alarm, colorScheme: appTheme.theme) .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 12, trailing: 20)) .listRowSeparator(.hidden) .listRowBackground(Color.clear) diff --git a/today-s-sound/Presentation/Features/OnBoarding/OnBoardingView.swift b/today-s-sound/Presentation/Features/OnBoarding/OnBoardingView.swift index a1ba3d4..a1e3a46 100644 --- a/today-s-sound/Presentation/Features/OnBoarding/OnBoardingView.swift +++ b/today-s-sound/Presentation/Features/OnBoarding/OnBoardingView.swift @@ -9,7 +9,7 @@ import SwiftUI struct OnBoardingView: View { @EnvironmentObject var session: SessionStore - @Environment(\.colorScheme) private var colorScheme + @EnvironmentObject var appTheme: AppThemeManager @State private var isLoading = false @State private var didStartRegistration = false @@ -18,7 +18,7 @@ struct OnBoardingView: View { VStack(spacing: 100) { Text("오늘의 소리") .font(.KoddiBold56) - .foregroundColor(colorScheme == .dark ? .white : .black) + .foregroundColor(Color.text(appTheme.theme)) .accessibilityAddTraits(.isHeader) VStack(spacing: 20) { @@ -52,7 +52,7 @@ struct OnBoardingView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(colorScheme == .dark ? Color.black : Color.white) + .background(Color.background(appTheme.theme)) .task { guard !didStartRegistration else { return } didStartRegistration = true @@ -68,11 +68,15 @@ struct OnBoardingView_Previews: PreviewProvider { Group { OnBoardingView() .environmentObject(SessionStore.preview) - .preferredColorScheme(.light) + .environmentObject(AppThemeManager()) OnBoardingView() .environmentObject(SessionStore.preview) - .preferredColorScheme(.dark) + .environmentObject({ + let manager = AppThemeManager() + manager.theme = .highContrast + return manager + }()) } } } diff --git a/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift b/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift index 7c7908d..261445a 100644 --- a/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift +++ b/today-s-sound/Presentation/Features/Settings/ContactDeveloperView.swift @@ -1,18 +1,18 @@ import SwiftUI struct ContactDeveloperView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appTheme: AppThemeManager @Environment(\.dismiss) var dismiss @State private var emailSubject = "" @State private var emailBody = "" var body: some View { ZStack { - Color.background(colorScheme) + Color.background(appTheme.theme) .ignoresSafeArea() VStack(spacing: 0) { - ScreenMainTitle(text: "개발자 문의", colorScheme: colorScheme) + ScreenMainTitle(text: "개발자 문의", theme: appTheme.theme) .padding(.top, 16) Spacer() @@ -22,12 +22,12 @@ struct ContactDeveloperView: View { VStack(spacing: 12) { Text("문의사항이 있으신가요?") .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) .accessibilityLabel("문의사항이 있으신가요?") Text("아래 이메일로 문의해주세요.") .font(.KoddiRegular16) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) .accessibilityLabel("아래 이메일로 문의해주세요.") } @@ -71,7 +71,7 @@ struct ContactDeveloperView: View { } label: { Image(systemName: "chevron.left") .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) } .accessibilityLabel("뒤로 가기") .accessibilityHint("관리 페이지로 돌아갑니다") diff --git a/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift b/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift index e646b33..d3d7152 100644 --- a/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift +++ b/today-s-sound/Presentation/Features/Settings/PlaybackSettingsView.swift @@ -2,16 +2,16 @@ import SwiftUI struct PlaybackSettingsView: View { @StateObject private var viewModel = PlaybackSettingsViewModel() - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appTheme: AppThemeManager @Environment(\.dismiss) var dismiss var body: some View { ZStack { - Color.background(colorScheme) + Color.background(appTheme.theme) .ignoresSafeArea() VStack(spacing: 0) { - ScreenMainTitle(text: "재생 설정", colorScheme: colorScheme) + ScreenMainTitle(text: "재생 설정", theme: appTheme.theme) .padding(.top, 16) // 재생 속도 설정 @@ -37,7 +37,7 @@ struct PlaybackSettingsView: View { Text(String(format: "%.1f x", viewModel.playbackRate)) .font(.KoddiBold48) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) .monospacedDigit() .frame(minWidth: 100) .accessibilityElement() @@ -60,7 +60,7 @@ struct PlaybackSettingsView: View { } Divider() - .background(Color.border(colorScheme)) + .background(Color.border(appTheme.theme)) .padding(.horizontal, 20) Spacer() @@ -93,7 +93,7 @@ struct PlaybackSettingsView: View { } label: { Image(systemName: "chevron.left") .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) } .accessibilityLabel("뒤로 가기") .accessibilityHint("관리 페이지로 돌아갑니다") diff --git a/today-s-sound/Presentation/Features/Settings/SettingsView.swift b/today-s-sound/Presentation/Features/Settings/SettingsView.swift index 70e585b..30bda47 100644 --- a/today-s-sound/Presentation/Features/Settings/SettingsView.swift +++ b/today-s-sound/Presentation/Features/Settings/SettingsView.swift @@ -2,18 +2,17 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var session: SessionStore - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appTheme: AppThemeManager @State private var showDeleteAlert = false - @AppStorage("highContrastMode") private var highContrastMode = false var body: some View { NavigationView { ZStack { - Color.background(colorScheme) + Color.background(appTheme.theme) .ignoresSafeArea() VStack(spacing: 0) { - ScreenMainTitle(text: "관리", colorScheme: colorScheme) + ScreenMainTitle(text: "관리", theme: appTheme.theme) .padding(.top, 16) Spacer() @@ -21,48 +20,51 @@ struct SettingsView: View { // 관리 항목 리스트 VStack(spacing: 0) { NavigationLink(destination: SubscriptionListView()) { - SettingsRow(title: "구독 페이지 관리", colorScheme: colorScheme) + SettingsRow(title: "구독 페이지 관리", theme: appTheme.theme) } .buttonStyle(PlainButtonStyle()) Divider() - .background(Color.border(colorScheme)) + .background(Color.border(appTheme.theme)) .padding(.horizontal, 20) NavigationLink(destination: PlaybackSettingsView()) { - SettingsRow(title: "재생 설정", colorScheme: colorScheme) + SettingsRow(title: "재생 설정", theme: appTheme.theme) } .buttonStyle(PlainButtonStyle()) Divider() - .background(Color.border(colorScheme)) + .background(Color.border(appTheme.theme)) .padding(.horizontal, 20) NavigationLink(destination: ContactDeveloperView()) { - SettingsRow(title: "개발자에게 문의", colorScheme: colorScheme) + SettingsRow(title: "개발자에게 문의", theme: appTheme.theme) } .buttonStyle(PlainButtonStyle()) Divider() - .background(Color.border(colorScheme)) + .background(Color.border(appTheme.theme)) .padding(.horizontal, 20) HStack { Text("고대비 모드 설정") .font(.KoddiBold24) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) Spacer() - Toggle("", isOn: $highContrastMode) - .labelsHidden() + Toggle("", isOn: Binding( + get: { appTheme.isHighContrast }, + set: { _ in appTheme.toggleTheme() } + )) + .labelsHidden() } .padding(.horizontal, 20) .padding(.vertical, 16) .accessibilityElement(children: .combine) .accessibilityLabel("고대비 모드 설정") - .accessibilityValue(highContrastMode ? "켜짐" : "꺼짐") + .accessibilityValue(appTheme.isHighContrast ? "켜짐" : "꺼짐") .accessibilityHint("탭하여 고대비 모드 설정을 변경합니다") } - .background(Color.background(colorScheme)) + .background(Color.background(appTheme.theme)) .cornerRadius(12) .padding(.horizontal, 20) .padding(.bottom, 40) @@ -108,13 +110,13 @@ struct SettingsView: View { struct SettingsRow: View { let title: String - let colorScheme: ColorScheme + let theme: AppTheme var body: some View { HStack { Text(title) .font(.KoddiBold24) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(theme)) Spacer() } .padding(.horizontal, 20) diff --git a/today-s-sound/Presentation/Features/SubscriptionList/Component/StatusBadge.swift b/today-s-sound/Presentation/Features/SubscriptionList/Component/StatusBadge.swift index 5c9097a..af5df61 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/Component/StatusBadge.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/Component/StatusBadge.swift @@ -9,7 +9,7 @@ import SwiftUI struct StatusBadge: View { let text: String - let colorScheme: ColorScheme + // colorScheme 파라미터는 실제로 사용되지 않지만, 호출부와의 호환성을 위해 유지 var body: some View { Text(text) @@ -28,8 +28,8 @@ struct StatusBadge: View { struct StatusBadge_Previews: PreviewProvider { static var previews: some View { VStack(spacing: 16) { - StatusBadge(text: "등록중", colorScheme: .light) - StatusBadge(text: "일이삼사", colorScheme: .dark) + StatusBadge(text: "등록중", theme: .normal) + StatusBadge(text: "일이삼사", theme: .highContrast) } .padding() } diff --git a/today-s-sound/Presentation/Features/SubscriptionList/Component/SubscriptionCardView.swift b/today-s-sound/Presentation/Features/SubscriptionList/Component/SubscriptionCardView.swift index 74bf4ee..f8a476b 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/Component/SubscriptionCardView.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/Component/SubscriptionCardView.swift @@ -9,7 +9,7 @@ import SwiftUI struct SubscriptionCardView: View { let subscription: SubscriptionItem - let colorScheme: ColorScheme + let colorScheme: AppTheme var onToggleAlarm: ((SubscriptionItem) -> Void)? var body: some View { @@ -90,17 +90,17 @@ struct SubscriptionCardView_Previews: PreviewProvider { Group { SubscriptionCardView( subscription: sampleSubscription, - colorScheme: .light + colorScheme: .normal ) .padding() - .previewDisplayName("Light") + .previewDisplayName("Normal") SubscriptionCardView( subscription: sampleSubscription, - colorScheme: .dark + colorScheme: .highContrast ) .padding() - .previewDisplayName("Dark") + .previewDisplayName("High Contrast") .background(Color.black) } .previewLayout(.sizeThatFits) diff --git a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift index 05892a6..ac5e8bc 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift @@ -2,7 +2,7 @@ import SwiftUI struct SubscriptionListView: View { @StateObject private var viewModel: SubscriptionListViewModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var appTheme: AppThemeManager @Environment(\.dismiss) var dismiss @State private var showAddSubscription = false @@ -12,12 +12,12 @@ struct SubscriptionListView: View { var body: some View { ZStack { - Color.background(colorScheme) + Color.background(appTheme.theme) .ignoresSafeArea() VStack(spacing: 12) { - ScreenMainTitle(text: "구독 페이지 관리", colorScheme: colorScheme) - ScreenSubTitle(text: "구독 중인 페이지", colorScheme: colorScheme) + ScreenMainTitle(text: "구독 페이지 관리", theme: appTheme.theme) + ScreenSubTitle(text: "구독 중인 페이지", theme: appTheme.theme) .padding(.top, 16) // 로딩 상태 @@ -36,7 +36,7 @@ struct SubscriptionListView: View { VStack(spacing: 16) { Text(errorMessage) .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) .accessibilityLabel("오류: \(errorMessage)") .padding(.bottom) @@ -60,7 +60,7 @@ struct SubscriptionListView: View { VStack(spacing: 16) { Text("구독 중인 페이지가 없습니다") .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(appTheme.theme)) .accessibilityLabel("구독 중인 페이지가 없습니다") } Spacer() @@ -72,7 +72,7 @@ struct SubscriptionListView: View { ForEach(viewModel.subscriptions) { subscription in SubscriptionCardView( subscription: subscription, - colorScheme: colorScheme, + colorScheme: appTheme.theme, onToggleAlarm: { sub in if sub.isUrgent { viewModel.blockAlarm(sub) @@ -120,7 +120,7 @@ struct SubscriptionListView: View { } AddSubscriptionButton( title: "새 웹페이지 추가", - colorScheme: colorScheme, + colorScheme: appTheme.theme, isEnabled: true ) { showAddSubscription = true @@ -139,7 +139,7 @@ struct SubscriptionListView: View { } label: { Image(systemName: "chevron.left") .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(appTheme.theme)) } .accessibilityLabel("뒤로 가기") .accessibilityHint("관리 페이지로 돌아갑니다") diff --git a/today-s-sound/Resources/Colors.swift b/today-s-sound/Resources/Colors.swift index cebf6b3..136b3e5 100644 --- a/today-s-sound/Resources/Colors.swift +++ b/today-s-sound/Resources/Colors.swift @@ -35,34 +35,73 @@ extension Color { // MARK: - Semantic Colors extension Color { - /// 배경색 (다크모드 대응) + /// 배경색 (앱 테마 기반) + static func background(_ theme: AppTheme) -> Color { + theme == .highContrast ? .black : .white + } + + /// 보조 배경색 (앱 테마 기반) + static func secondaryBackground(_ theme: AppTheme) -> Color { + theme == .highContrast ? Color(white: 0.15) : Color(white: 0.95) + } + + /// 텍스트 색상 (앱 테마 기반) + static func text(_ theme: AppTheme) -> Color { + theme == .highContrast ? .white : .black + } + + /// 보조 텍스트 색상 (앱 테마 기반) + static func secondaryText(_ theme: AppTheme) -> Color { + theme == .highContrast ? .white.opacity(0.6) : .black.opacity(0.6) + } + + /// 테두리 색상 (앱 테마 기반) + static func border(_ theme: AppTheme) -> Color { + theme == .highContrast ? .white.opacity(0.2) : .gray.opacity(0.3) + } + + /// 버튼 배경색 (앱 테마 기반) + static func buttonBackground(_ theme: AppTheme) -> Color { + theme == .highContrast ? .black : .white + } + + // MARK: - 기존 호환성을 위한 ColorScheme 기반 메서드 (deprecated) + // 기존 코드와의 호환성을 위해 유지하되, 내부적으로는 AppThemeManager를 사용하도록 권장 + + /// 배경색 (다크모드 대응) - deprecated: AppTheme 사용 권장 + @available(*, deprecated, message: "Use background(_ theme: AppTheme) instead") static func background(_ colorScheme: ColorScheme) -> Color { - colorScheme == .dark ? .black : .white + .white } - /// 보조 배경색 (다크모드 대응) + /// 보조 배경색 (다크모드 대응) - deprecated: AppTheme 사용 권장 + @available(*, deprecated, message: "Use secondaryBackground(_ theme: AppTheme) instead") static func secondaryBackground(_ colorScheme: ColorScheme) -> Color { - colorScheme == .dark ? Color(white: 0.15) : Color(white: 0.95) + Color(white: 0.95) } - /// 텍스트 색상 (다크모드 대응) + /// 텍스트 색상 (다크모드 대응) - deprecated: AppTheme 사용 권장 + @available(*, deprecated, message: "Use text(_ theme: AppTheme) instead") static func text(_ colorScheme: ColorScheme) -> Color { - colorScheme == .dark ? .white : .black + .black } - /// 보조 텍스트 색상 (다크모드 대응) + /// 보조 텍스트 색상 (다크모드 대응) - deprecated: AppTheme 사용 권장 + @available(*, deprecated, message: "Use secondaryText(_ theme: AppTheme) instead") static func secondaryText(_ colorScheme: ColorScheme) -> Color { - colorScheme == .dark ? .white.opacity(0.6) : .black.opacity(0.6) + .black.opacity(0.6) } - /// 테두리 색상 (다크모드 대응) + /// 테두리 색상 (다크모드 대응) - deprecated: AppTheme 사용 권장 + @available(*, deprecated, message: "Use border(_ theme: AppTheme) instead") static func border(_ colorScheme: ColorScheme) -> Color { - colorScheme == .dark ? .white.opacity(0.2) : .gray.opacity(0.3) + .gray.opacity(0.3) } - /// 버튼 배경색 (다크모드 대응) + /// 버튼 배경색 (다크모드 대응) - deprecated: AppTheme 사용 권장 + @available(*, deprecated, message: "Use buttonBackground(_ theme: AppTheme) instead") static func buttonBackground(_ colorScheme: ColorScheme) -> Color { - colorScheme == .dark ? .black : .white + .white } } From 6a3eaa4eeea9b00600d693cf5ab1e6e5ced6423a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=98=84?= <102128060+wlgusqkr@users.noreply.github.com> Date: Sun, 14 Dec 2025 14:05:38 +0900 Subject: [PATCH 7/7] temp fix --- .../Core/AppState/AppThemeManager.swift | 9 ++--- .../Component/AddSubscriptionButton.swift | 6 ++-- .../AddSubscription/AddSubscriptionView.swift | 33 ++++++++++--------- .../Component/InputFieldSection.swift | 24 +++++++------- .../Component/KeywordCheckboxRow.swift | 10 +++--- .../Component/SheetHandler.swift | 8 ++--- .../Presentation/Features/Feed/FeedView.swift | 22 ++++++++----- .../Presentation/Features/Main/MainView.swift | 12 +++---- .../NotificationList/AlertCardView.swift | 6 ++-- .../NotificationListView.swift | 2 +- .../Component/StatusBadge.swift | 3 +- .../Component/SubscriptionCardView.swift | 10 +++--- .../SubscriptionListView.swift | 4 +-- today-s-sound/Resources/Colors.swift | 1 + 14 files changed, 79 insertions(+), 71 deletions(-) diff --git a/today-s-sound/Core/AppState/AppThemeManager.swift b/today-s-sound/Core/AppState/AppThemeManager.swift index 8666beb..2425c22 100644 --- a/today-s-sound/Core/AppState/AppThemeManager.swift +++ b/today-s-sound/Core/AppState/AppThemeManager.swift @@ -9,8 +9,8 @@ import SwiftUI /// 앱 테마 모드 enum AppTheme: String, CaseIterable { - case normal = "normal" - case highContrast = "highContrast" + case normal + case highContrast } /// 앱 테마 관리자 @@ -26,11 +26,12 @@ final class AppThemeManager: ObservableObject { init() { // UserDefaults에서 저장된 테마 불러오기 if let savedTheme = UserDefaults.standard.string(forKey: "appTheme"), - let theme = AppTheme(rawValue: savedTheme) { + let theme = AppTheme(rawValue: savedTheme) + { self.theme = theme } else { // 기본값은 일반 모드 - self.theme = .normal + theme = .normal } } diff --git a/today-s-sound/Presentation/Base/Component/AddSubscriptionButton.swift b/today-s-sound/Presentation/Base/Component/AddSubscriptionButton.swift index 24da3d5..4f115ad 100644 --- a/today-s-sound/Presentation/Base/Component/AddSubscriptionButton.swift +++ b/today-s-sound/Presentation/Base/Component/AddSubscriptionButton.swift @@ -12,7 +12,7 @@ struct AddSubscriptionButton: View { let title: String /// 앱 테마 (고대비/일반 모드에 따라 글자색만 바뀜) - let colorScheme: AppTheme + let theme: AppTheme /// 버튼 활성/비활성 여부 let isEnabled: Bool @@ -55,7 +55,7 @@ struct AddSubscriptionButton_Previews: PreviewProvider { // Normal Mode AddSubscriptionButton( title: "등록 승인 요청", - colorScheme: .normal, + theme: .normal, isEnabled: true, action: {} ) @@ -67,7 +67,7 @@ struct AddSubscriptionButton_Previews: PreviewProvider { // High Contrast Mode AddSubscriptionButton( title: "등록 승인 요청", - colorScheme: .highContrast, + theme: .highContrast, isEnabled: true, action: {} ) diff --git a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift index d43f640..adb5049 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift @@ -16,7 +16,7 @@ struct AddSubscriptionView: View { VStack(spacing: 0) { // 상단 핸들 바 (X 대신) - SheetHandleBar(colorScheme: appTheme.theme) + SheetHandleBar(theme: appTheme.theme) .accessibilityElement() .accessibilityLabel("새 웹페이지 추가 창 닫기") .accessibilityHint("이 영역을 두 번 탭하거나 아래로 스와이프하면 창이 닫힙니다.") @@ -41,7 +41,7 @@ struct AddSubscriptionView: View { description: "모니터링할 웹페이지의 정확한 URL을 입력하세요.", isRequired: true, text: $viewModel.urlText, - colorScheme: appTheme.theme + theme: appTheme.theme ) // 2) 웹페이지 별명 (선택) @@ -50,7 +50,7 @@ struct AddSubscriptionView: View { description: "해당 페이지를 식별할 명칭을 입력하세요.", isRequired: false, text: $viewModel.nameText, - colorScheme: appTheme.theme + theme: appTheme.theme ) // 3) 키워드 필터 @@ -96,7 +96,7 @@ struct AddSubscriptionView: View { ForEach(viewModel.selectedKeywords, id: \.self) { keyword in KeywordBadgeWithDelete( text: keyword, - colorScheme: appTheme.theme + theme: appTheme.theme ) { viewModel.removeKeyword(keyword) } @@ -130,7 +130,7 @@ struct AddSubscriptionView: View { // 하단 고정 "등록 승인 요청" 버튼 AddSubscriptionButton( title: viewModel.isLoading ? "등록 중..." : "등록 승인 요청", - colorScheme: appTheme.theme, + theme: appTheme.theme, isEnabled: viewModel.isSubmitEnabled && !viewModel.isLoading ) { viewModel.createSubscription { success in @@ -167,7 +167,7 @@ struct AddSubscriptionView: View { .ignoresSafeArea(.keyboard, edges: .bottom) // 키워드 설정 시트 .sheet(isPresented: $viewModel.showKeywordSelector) { - KeywordSelectorSheet(viewModel: viewModel, colorScheme: appTheme.theme) + KeywordSelectorSheet(viewModel: viewModel, theme: appTheme.theme) .onAppear { // 키워드 설정 시트가 열릴 때 키워드 목록 로드 if viewModel.availableKeywords.isEmpty { @@ -198,19 +198,19 @@ struct AddSubscriptionView: View { struct KeywordSelectorSheet: View { @ObservedObject var viewModel: AddSubscriptionViewModel - let colorScheme: AppTheme + let theme: AppTheme @Environment(\.dismiss) var dismiss var body: some View { ZStack { - Color.background(colorScheme) + Color.background(theme) .ignoresSafeArea() .onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } VStack(spacing: 0) { - SheetHandleBar(colorScheme: colorScheme) + SheetHandleBar(theme: theme) .padding(.top, 20) .accessibilityElement() .accessibilityLabel("키워드 설정 창 닫기") @@ -220,7 +220,7 @@ struct KeywordSelectorSheet: View { } // 키워드 설정 화면 제목 - ScreenSubTitle(text: "키워드 설정", theme: colorScheme) + ScreenSubTitle(text: "키워드 설정", theme: theme) VStack(spacing: 0) { // 스크롤 가능한 키워드 목록 @@ -238,7 +238,7 @@ struct KeywordSelectorSheet: View { VStack(spacing: 16) { Text(errorMessage) .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(theme)) .accessibilityLabel("오류: \(errorMessage)") Button("다시 시도") { @@ -259,7 +259,7 @@ struct KeywordSelectorSheet: View { VStack(spacing: 16) { Text("등록된 키워드가 없습니다.") .font(.KoddiBold20) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(theme)) .accessibilityLabel("등록된 키워드가 없습니다") } .frame(maxWidth: .infinity) @@ -270,14 +270,14 @@ struct KeywordSelectorSheet: View { KeywordCheckboxRow( keyword: keyword, isSelected: viewModel.selectedKeywords.contains(keyword), - colorScheme: colorScheme + theme: theme ) { viewModel.toggleKeyword(keyword) } if index < viewModel.availableKeywords.count - 1 { Divider() - .background(Color.border(colorScheme)) + .background(Color.border(theme)) .padding(.horizontal, 20) } } @@ -293,7 +293,7 @@ struct KeywordSelectorSheet: View { // 하단 고정 "저장하기" 버튼 AddSubscriptionButton( title: "저장하기", - colorScheme: colorScheme, + theme: theme, isEnabled: true ) { dismiss() @@ -312,8 +312,9 @@ struct KeywordSelectorSheet: View { /// 삭제 버튼이 있는 키워드 배지 struct KeywordBadgeWithDelete: View { let text: String - let colorScheme: AppTheme + let theme: AppTheme let onDelete: () -> Void + // theme 파라미터는 현재 사용되지 않지만, 향후 테마 적용을 위해 유지 var body: some View { HStack(spacing: 6) { diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift index be014dd..0cd7730 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/InputFieldSection.swift @@ -10,7 +10,7 @@ struct InputFieldSection: View { let description: String let isRequired: Bool @Binding var text: String - let colorScheme: AppTheme + let theme: AppTheme let additionalContent: (() -> AnyView)? init( @@ -18,14 +18,14 @@ struct InputFieldSection: View { description: String, isRequired: Bool = false, text: Binding, - colorScheme: AppTheme, + theme: AppTheme, additionalContent: (() -> AnyView)? = nil ) { self.title = title self.description = description self.isRequired = isRequired _text = text - self.colorScheme = colorScheme + self.theme = theme self.additionalContent = additionalContent } @@ -35,7 +35,7 @@ struct InputFieldSection: View { HStack(spacing: 4) { Text(title) .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(theme)) .accessibilityLabel(isRequired ? "\(title) 필수 입력" : title) if isRequired { @@ -53,16 +53,16 @@ struct InputFieldSection: View { .keyboardType(isRequired ? .URL : .default) .padding(.horizontal, 18) .padding(.vertical, 16) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(theme)) .font(.KoddiRegular16) .contentShape(Rectangle()) .background( RoundedRectangle(cornerRadius: 8) - .fill(Color.secondaryBackground(colorScheme)) + .fill(Color.secondaryBackground(theme)) ) .overlay( RoundedRectangle(cornerRadius: 8) - .stroke(Color.border(colorScheme), lineWidth: 1) + .stroke(Color.border(theme), lineWidth: 1) ) .accessibilityLabel("\(title) 편집창") .accessibilityHint(description) @@ -76,7 +76,7 @@ struct InputFieldSection: View { // 설명 텍스트 Text(description) .font(.KoddiRegular16) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(theme)) .accessibilityLabel(description) } } @@ -93,7 +93,7 @@ struct InputFieldSection_Previews: PreviewProvider { description: "모니터링할 웹페이지 URL을 입력하세요.", isRequired: true, text: .constant(""), - colorScheme: .normal + theme: .normal ) InputFieldSection( @@ -101,7 +101,7 @@ struct InputFieldSection_Previews: PreviewProvider { description: "해당 페이지를 식별할 명칭을 입력하세요. (선택 사항)", isRequired: false, text: .constant("이미 입력된 값"), - colorScheme: .normal + theme: .normal ) } .padding() @@ -114,7 +114,7 @@ struct InputFieldSection_Previews: PreviewProvider { description: "모니터링할 웹페이지 URL을 입력하세요.", isRequired: true, text: .constant(""), - colorScheme: .highContrast + theme: .highContrast ) InputFieldSection( @@ -122,7 +122,7 @@ struct InputFieldSection_Previews: PreviewProvider { description: "해당 페이지를 식별할 명칭을 입력하세요. (선택 사항)", isRequired: false, text: .constant("이미 입력된 값"), - colorScheme: .highContrast + theme: .highContrast ) } .padding() diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift index c22aeac..21cd79e 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift @@ -9,7 +9,7 @@ import SwiftUI struct KeywordCheckboxRow: View { let keyword: String let isSelected: Bool - let colorScheme: AppTheme + let theme: AppTheme let action: () -> Void var body: some View { @@ -17,7 +17,7 @@ struct KeywordCheckboxRow: View { HStack(spacing: 16) { ZStack { RoundedRectangle(cornerRadius: 6) - .stroke(isSelected ? Color.primaryGreen : Color.border(colorScheme), lineWidth: 2) + .stroke(isSelected ? Color.primaryGreen : Color.border(theme), lineWidth: 2) .frame(width: 28, height: 28) if isSelected { @@ -34,7 +34,7 @@ struct KeywordCheckboxRow: View { Text(keyword) .font(.KoddiBold20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(theme)) Spacer() } @@ -54,14 +54,14 @@ struct KeywordCheckboxRow_Previews: PreviewProvider { KeywordCheckboxRow( keyword: "시각장애", isSelected: true, - colorScheme: .normal, + theme: .normal, action: {} ) KeywordCheckboxRow( keyword: "접근성", isSelected: false, - colorScheme: .highContrast, + theme: .highContrast, action: {} ) } diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/SheetHandler.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/SheetHandler.swift index 00438ae..740e2ad 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/Component/SheetHandler.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/SheetHandler.swift @@ -9,12 +9,12 @@ import SwiftUI /// 시트 상단에 보이는 작은 핸들 바 struct SheetHandleBar: View { - let colorScheme: AppTheme + let theme: AppTheme var body: some View { VStack(spacing: 8) { Capsule() - .fill(Color.secondaryText(colorScheme).opacity(0.3)) + .fill(Color.secondaryText(theme).opacity(0.3)) .frame(width: 80, height: 5) .padding(.top, 8) .accessibilityHidden(true) @@ -29,8 +29,8 @@ struct SheetHandleBar: View { struct SheetHandleBar_Previews: PreviewProvider { static var previews: some View { VStack { - SheetHandleBar(colorScheme: .normal) - SheetHandleBar(colorScheme: .highContrast) + SheetHandleBar(theme: .normal) + SheetHandleBar(theme: .highContrast) } .background(Color.background(.normal)) } diff --git a/today-s-sound/Presentation/Features/Feed/FeedView.swift b/today-s-sound/Presentation/Features/Feed/FeedView.swift index af6f84d..6be024d 100644 --- a/today-s-sound/Presentation/Features/Feed/FeedView.swift +++ b/today-s-sound/Presentation/Features/Feed/FeedView.swift @@ -119,7 +119,7 @@ struct FeedView: View { // 필터된 카드 리스트 ForEach(filteredItems) { item in - FeedCard(item: item, colorScheme: appTheme.theme) + FeedCard(item: item, theme: appTheme.theme) .padding(.horizontal, 16) .onAppear { viewModel.loadMoreIfNeeded(currentItem: item) @@ -199,26 +199,26 @@ struct FeedView: View { private struct FeedCard: View { let item: FeedItem - let colorScheme: AppTheme + let theme: AppTheme var body: some View { VStack(alignment: .leading, spacing: 12) { // 페이지 이름 (작은 회색 텍스트) Text(item.alias) .font(.KoddiRegular16) - .foregroundColor(Color.secondaryText(colorScheme)) + .foregroundColor(Color.secondaryText(theme)) // 제목 (큰 볼드 텍스트, 두 줄 가능) Text(item.summaryTitle) .font(.KoddiBold28) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(theme)) .multilineTextAlignment(.leading) .lineLimit(2) // 내용 (중간 크기 텍스트, 여러 줄 가능) Text(item.summary) .font(.KoddiRegular20) - .foregroundColor(Color.text(colorScheme)) + .foregroundColor(Color.text(theme)) .multilineTextAlignment(.leading) .lineLimit(nil) @@ -231,11 +231,11 @@ private struct FeedCard: View { .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 16) - .fill(Color.secondaryBackground(colorScheme)) + .fill(Color.secondaryBackground(theme)) ) .overlay( RoundedRectangle(cornerRadius: 16) - .stroke(Color.border(colorScheme), lineWidth: 1) + .stroke(Color.border(theme), lineWidth: 1) ) .accessibilityElement(children: .combine) .accessibilityLabel( @@ -250,10 +250,14 @@ struct FeedView_Previews: PreviewProvider { static var previews: some View { Group { FeedView() - .environment(\.colorScheme, .light) + .environmentObject(AppThemeManager()) FeedView() - .environment(\.colorScheme, .dark) + .environmentObject({ + let manager = AppThemeManager() + manager.theme = .highContrast + return manager + }()) } } } diff --git a/today-s-sound/Presentation/Features/Main/MainView.swift b/today-s-sound/Presentation/Features/Main/MainView.swift index 3141add..3e7790b 100644 --- a/today-s-sound/Presentation/Features/Main/MainView.swift +++ b/today-s-sound/Presentation/Features/Main/MainView.swift @@ -71,19 +71,19 @@ struct MainView: View { private func setupTabBarAppearance() { let appearance = UITabBarAppearance() - + // TabBar 배경색을 앱 테마에 맞게 설정 if appTheme.isHighContrast { // 고대비 모드: 검은 배경 appearance.configureWithOpaqueBackground() appearance.backgroundColor = .black - + // 선택되지 않은 탭 아이템 색상 appearance.stackedLayoutAppearance.normal.iconColor = .white.withAlphaComponent(0.6) appearance.stackedLayoutAppearance.normal.titleTextAttributes = [ .foregroundColor: UIColor.white.withAlphaComponent(0.6) ] - + // 선택된 탭 아이템 색상 appearance.stackedLayoutAppearance.selected.iconColor = .white appearance.stackedLayoutAppearance.selected.titleTextAttributes = [ @@ -93,20 +93,20 @@ struct MainView: View { // 일반 모드: 흰 배경 appearance.configureWithOpaqueBackground() appearance.backgroundColor = .white - + // 선택되지 않은 탭 아이템 색상 appearance.stackedLayoutAppearance.normal.iconColor = .black.withAlphaComponent(0.6) appearance.stackedLayoutAppearance.normal.titleTextAttributes = [ .foregroundColor: UIColor.black.withAlphaComponent(0.6) ] - + // 선택된 탭 아이템 색상 appearance.stackedLayoutAppearance.selected.iconColor = UIColor(Color.primaryGreen) appearance.stackedLayoutAppearance.selected.titleTextAttributes = [ .foregroundColor: UIColor(Color.primaryGreen) ] } - + UITabBar.appearance().standardAppearance = appearance if #available(iOS 15.0, *) { UITabBar.appearance().scrollEdgeAppearance = appearance diff --git a/today-s-sound/Presentation/Features/NotificationList/AlertCardView.swift b/today-s-sound/Presentation/Features/NotificationList/AlertCardView.swift index 89a58f2..1c89e49 100644 --- a/today-s-sound/Presentation/Features/NotificationList/AlertCardView.swift +++ b/today-s-sound/Presentation/Features/NotificationList/AlertCardView.swift @@ -7,7 +7,7 @@ import SwiftUI struct AlertCardView: View { let alarm: AlarmItem - let colorScheme: AppTheme + let theme: AppTheme private var cardColor: Color { alarm.isUrgent ? .urgentPink : .primaryGreen @@ -82,13 +82,13 @@ struct AlertCardView_Previews: PreviewProvider { static var previews: some View { Group { ForEach(sampleAlarms) { alarm in - AlertCardView(alarm: alarm, colorScheme: .normal) + AlertCardView(alarm: alarm, theme: .normal) .padding() .previewDisplayName("Card Normal - \(alarm.alias)") } ForEach(sampleAlarms) { alarm in - AlertCardView(alarm: alarm, colorScheme: .highContrast) + AlertCardView(alarm: alarm, theme: .highContrast) .padding() .background(Color.black) .previewDisplayName("Card High Contrast - \(alarm.alias)") diff --git a/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift b/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift index 0cd72a9..378975a 100644 --- a/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift +++ b/today-s-sound/Presentation/Features/NotificationList/NotificationListView.swift @@ -79,7 +79,7 @@ struct NotificationListView: View { else { List { ForEach(viewModel.alarms) { alarm in - AlertCardView(alarm: alarm, colorScheme: appTheme.theme) + AlertCardView(alarm: alarm, theme: appTheme.theme) .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 12, trailing: 20)) .listRowSeparator(.hidden) .listRowBackground(Color.clear) diff --git a/today-s-sound/Presentation/Features/SubscriptionList/Component/StatusBadge.swift b/today-s-sound/Presentation/Features/SubscriptionList/Component/StatusBadge.swift index af5df61..b4f25b0 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/Component/StatusBadge.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/Component/StatusBadge.swift @@ -9,7 +9,8 @@ import SwiftUI struct StatusBadge: View { let text: String - // colorScheme 파라미터는 실제로 사용되지 않지만, 호출부와의 호환성을 위해 유지 + let theme: AppTheme + // theme 파라미터는 현재 사용되지 않지만, 향후 테마 적용을 위해 유지 var body: some View { Text(text) diff --git a/today-s-sound/Presentation/Features/SubscriptionList/Component/SubscriptionCardView.swift b/today-s-sound/Presentation/Features/SubscriptionList/Component/SubscriptionCardView.swift index f8a476b..cbdf7cc 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/Component/SubscriptionCardView.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/Component/SubscriptionCardView.swift @@ -9,7 +9,7 @@ import SwiftUI struct SubscriptionCardView: View { let subscription: SubscriptionItem - let colorScheme: AppTheme + let theme: AppTheme var onToggleAlarm: ((SubscriptionItem) -> Void)? var body: some View { @@ -32,7 +32,7 @@ struct SubscriptionCardView: View { if !subscription.keywords.isEmpty { HStack(spacing: 8) { ForEach(subscription.keywords.prefix(3)) { keyword in - StatusBadge(text: keyword.name, colorScheme: colorScheme) + StatusBadge(text: keyword.name, theme: theme) .accessibilityLabel("설정 키워드: \(keyword.name)") } @@ -40,7 +40,7 @@ struct SubscriptionCardView: View { if subscription.keywords.count > 3 { StatusBadge( text: "+\(subscription.keywords.count - 3)", - colorScheme: colorScheme + theme: theme ) .accessibilityLabel("그외 \(subscription.keywords.count - 3)개") } @@ -90,14 +90,14 @@ struct SubscriptionCardView_Previews: PreviewProvider { Group { SubscriptionCardView( subscription: sampleSubscription, - colorScheme: .normal + theme: .normal ) .padding() .previewDisplayName("Normal") SubscriptionCardView( subscription: sampleSubscription, - colorScheme: .highContrast + theme: .highContrast ) .padding() .previewDisplayName("High Contrast") diff --git a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift index ac5e8bc..13b6c0b 100644 --- a/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift +++ b/today-s-sound/Presentation/Features/SubscriptionList/SubscriptionListView.swift @@ -72,7 +72,7 @@ struct SubscriptionListView: View { ForEach(viewModel.subscriptions) { subscription in SubscriptionCardView( subscription: subscription, - colorScheme: appTheme.theme, + theme: appTheme.theme, onToggleAlarm: { sub in if sub.isUrgent { viewModel.blockAlarm(sub) @@ -120,7 +120,7 @@ struct SubscriptionListView: View { } AddSubscriptionButton( title: "새 웹페이지 추가", - colorScheme: appTheme.theme, + theme: appTheme.theme, isEnabled: true ) { showAddSubscription = true diff --git a/today-s-sound/Resources/Colors.swift b/today-s-sound/Resources/Colors.swift index 136b3e5..a3be88e 100644 --- a/today-s-sound/Resources/Colors.swift +++ b/today-s-sound/Resources/Colors.swift @@ -66,6 +66,7 @@ extension Color { } // MARK: - 기존 호환성을 위한 ColorScheme 기반 메서드 (deprecated) + // 기존 코드와의 호환성을 위해 유지하되, 내부적으로는 AppThemeManager를 사용하도록 권장 /// 배경색 (다크모드 대응) - deprecated: AppTheme 사용 권장