Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions today-s-sound/App/TodaySSoundApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -78,6 +79,7 @@ struct TodaySSoundApp: App {
}
}
.environmentObject(session)
.environmentObject(appTheme)
}
}
}
47 changes: 47 additions & 0 deletions today-s-sound/Core/AppState/AppThemeManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// AppThemeManager.swift
// today-s-sound
//
// Created by Assistant
//

import SwiftUI

/// 앱 테마 모드
enum AppTheme: String, CaseIterable {
case normal
case 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 {
// 기본값은 일반 모드
theme = .normal
}
}

/// 고대비 모드 활성화 여부
var isHighContrast: Bool {
theme == .highContrast
}

/// 테마 토글
func toggleTheme() {
theme = theme == .normal ? .highContrast : .normal
}
}
4 changes: 2 additions & 2 deletions today-s-sound/Core/Network/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions today-s-sound/Core/Network/Service/APIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ protocol APIServiceType {
func getSubscriptions(
userId: String, deviceSecret: String, page: Int, size: Int
) -> AnyPublisher<SubscriptionListResponse, NetworkError>
func createSubscription(
userId: String, deviceSecret: String, request: CreateSubscriptionRequest
) -> AnyPublisher<CreateSubscriptionResponse, NetworkError>
func deleteSubscription(
userId: String, deviceSecret: String, subscriptionId: Int64
) -> AnyPublisher<DeleteSubscriptionResponse, NetworkError>
Expand Down Expand Up @@ -175,6 +178,27 @@ class APIService: APIServiceType {
.eraseToAnyPublisher()
}

func createSubscription(
userId: String, deviceSecret: String, request: CreateSubscriptionRequest
) -> AnyPublisher<CreateSubscriptionResponse, NetworkError> {
subscriptionProvider.requestPublisher(.createSubscription(
userId: userId,
deviceSecret: deviceSecret,
request: request
))
.mapError { moyaError -> NetworkError in
.requestFailed(moyaError)
}
.flatMap { [weak self] response -> AnyPublisher<CreateSubscriptionResponse, NetworkError> 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(
Expand Down
14 changes: 14 additions & 0 deletions today-s-sound/Core/Network/Targets/SubscriptionAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -33,6 +36,8 @@ extension SubscriptionAPI: APITargetType {
switch self {
case .getSubscriptions:
.get
case .createSubscription:
.post
case .deleteSubscription:
.delete
case .blockAlarm, .unblockAlarm:
Expand All @@ -50,6 +55,8 @@ extension SubscriptionAPI: APITargetType {
],
encoding: URLEncoding.queryString
)
case let .createSubscription(_, _, request):
.requestJSONEncodable(request)
case .deleteSubscription, .blockAlarm, .unblockAlarm:
.requestPlain
}
Expand All @@ -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",
Expand Down
16 changes: 14 additions & 2 deletions today-s-sound/Data/Models/Subscription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,30 @@ import Foundation
struct CreateSubscriptionRequest: Codable {
let url: String
let keywords: [String]
let alias: String?
let isUrgent: Bool
}

// MARK: - Subscription Response Models

/// 구독 목록 응답
typealias SubscriptionListResponse = APIResponse<[SubscriptionItem]>

/// 구독 생성 응답
struct CreateSubscriptionResponse: Codable {
/// 구독 생성 결과
struct CreateSubscriptionResult: Codable {
let subscriptionId: Int64
}

/// 구독 생성 응답
typealias CreateSubscriptionResponse = APIResponse<CreateSubscriptionResult>

extension CreateSubscriptionResponse {
// 편의 속성: result의 subscriptionId에 직접 접근
var subscriptionId: Int64 {
result.subscriptionId
}
}

/// 구독 삭제 응답
struct DeleteSubscriptionResponse: Codable {
let message: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ struct AddSubscriptionButton: View {
/// 버튼에 표시할 텍스트 (예: "등록 승인 요청", "저장하기")
let title: String

/// 컬러 스킴 (라이트/다크에 따라 글자색만 바뀜)
let colorScheme: ColorScheme
/// 앱 테마 (고대비/일반 모드에 따라 글자색만 바뀜)
let theme: AppTheme

/// 버튼 활성/비활성 여부
let isEnabled: Bool
Expand All @@ -21,8 +21,8 @@ struct AddSubscriptionButton: View {
let action: () -> Void

private var textColor: Color {
// 배경색은 그대로 두고, 글자색만 모드에 따라 변경
colorScheme == .dark ? .black : .white
// 배경색은 그대로 두고, 글자색은 항상 흰색
.white
}

var body: some View {
Expand Down Expand Up @@ -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,
theme: .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,
theme: .highContrast,
isEnabled: true,
action: {}
)
.previewDisplayName("Dark Mode")
.previewDisplayName("High Contrast Mode")
.previewLayout(.sizeThatFits)
.padding()
.background(Color.background(.light))
.preferredColorScheme(.dark)
.background(Color.background(.highContrast))
}
}
}
10 changes: 5 additions & 5 deletions today-s-sound/Presentation/Base/Component/ScreenMainTitle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(.KoddiBold56)
.foregroundColor(Color.text(colorScheme))
.font(.KoddiBold48)
.foregroundColor(Color.text(theme))
.frame(maxWidth: .infinity, alignment: .center)
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import SwiftUI

struct ScreenSubTitle: View {
let text: String
let colorScheme: ColorScheme
let theme: AppTheme

var body: some View {
Text(text)
Expand All @@ -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)
}
}
}
Loading
Loading