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
24 changes: 12 additions & 12 deletions today-s-sound/App/TodaySSoundApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import SwiftUI

@main
struct TodaySSoundApp: App {
@StateObject private var session = SessionStore()

var body: some Scene {
WindowGroup {
Group {
if session.isRegistered {
MainView()
} else {
OnBoardingView()
}
}
.environmentObject(session)
@StateObject private var session = SessionStore()

var body: some Scene {
WindowGroup {
Group {
if session.isRegistered {
MainView()
} else {
OnBoardingView()
}
}
.environmentObject(session)
}
}
}
128 changes: 128 additions & 0 deletions today-s-sound/Core/AppState/SessionStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// SessionStore.swift
// today-s-sound
//
// Refactored by Assistant - Moya 기반으로 리팩토링
//

import Combine
import Foundation
import SwiftUI

@MainActor
final class SessionStore: ObservableObject {
@Published private(set) var userId: String?
@Published private(set) var isRegistered: Bool = false
@Published var lastError: String?

private let apiService: APIService
private var cancellables = Set<AnyCancellable>()

init(apiService: APIService = APIService()) {
self.apiService = apiService

// 앱 시작 시, 키체인에 저장되어 있으면 로드
#if DEBUG
print("━━━━━━━━━━━━━━━━━━━━━━━━━━")
print("🔐 키체인 확인 (SessionStore.init)")
print("━━━━━━━━━━━━━━━━━━━━━━━━━━")
if let deviceSecret = Keychain.getString(for: KeychainKey.deviceSecret) {
print("✅ deviceSecret: \(deviceSecret)")
} else {
print("❌ deviceSecret: (없음)")
}
if let savedId = Keychain.getString(for: KeychainKey.userId) {
print("✅ userId: \(savedId)")
} else {
print("❌ userId: (없음)")
}
print("━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
#else
print("⚠️ RELEASE 모드로 실행 중 - DEBUG 로그 비활성화")
#endif

if let savedId = Keychain.getString(for: KeychainKey.userId) {
userId = savedId
isRegistered = true
} else {
isRegistered = false
}
}

/// 처음 실행 시 한 번 호출: deviceSecret 생성/보관 → 서버 등록
func registerIfNeeded() async {
guard !isRegistered else { return }

// 1) deviceSecret을 키체인에서 찾고, 없으면 생성하여 저장
let secret: String = {
if let savedSecret = Keychain.getString(for: KeychainKey.deviceSecret) {
return savedSecret
}
let generated = DeviceSecretGenerator.generate()
Keychain.setString(generated, for: KeychainKey.deviceSecret)
return generated
}()

// 2) Combine을 사용한 비동기 API 호출
await withCheckedContinuation { continuation in
apiService.registerAnonymous(deviceSecret: secret)
.sink(
receiveCompletion: { [weak self] completion in
guard let self else { return }

switch completion {
case .finished:
break

case let .failure(error):
// 에러 처리
switch error {
case let .serverError(statusCode):
lastError = "서버 오류 (상태: \(statusCode))"

case let .decodingFailed(decodeError):
lastError = "응답 처리 실패: \(decodeError.localizedDescription)"

case let .requestFailed(requestError):
lastError = "요청 실패: \(requestError.localizedDescription)"

case .invalidURL:
lastError = "잘못된 URL"

case .unknown:
lastError = "알 수 없는 오류"
}

print("❌ 익명 사용자 등록 실패: \(lastError ?? "")")
}

continuation.resume()
},
receiveValue: { [weak self] response in
guard let self else { return }

// 3) userId 저장
let userId = response.result.userId
Keychain.setString(userId, for: KeychainKey.userId)

self.userId = userId
isRegistered = true
lastError = nil

print("✅ 익명 사용자 등록 성공: \(userId)")
}
)
.store(in: &self.cancellables)
}
}

/// 로그아웃 (키체인 초기화)
func logout() {
Keychain.delete(for: KeychainKey.userId)
Keychain.delete(for: KeychainKey.deviceSecret)

userId = nil
isRegistered = false
lastError = nil
}
}
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,9 +3,9 @@ import Foundation
enum Config {
static var baseURL: String {
#if DEBUG
return "https://dev-your-api-url.com"
return "http://localhost:8080" // 개발 서버
#else
return "https://your-api-url.com"
return "https://api.todays-sound.com" // 운영 서버
#endif
}
}
195 changes: 188 additions & 7 deletions today-s-sound/Core/Network/Service/APIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,93 @@ import Moya

protocol APIServiceType {
func request<T: Decodable>(_ target: some TargetType) -> AnyPublisher<T, NetworkError>
func createAnonymous(deviceSecret: String) -> AnyPublisher<AnonymousUserResponse, NetworkError>
func registerAnonymous(deviceSecret: String) -> AnyPublisher<RegisterAnonymousResponse, NetworkError>
func getSubscriptions(
userId: String, deviceSecret: String, page: Int, size: Int
) -> AnyPublisher<SubscriptionListResponse, NetworkError>
func getAlarms(
userId: String, deviceSecret: String, page: Int, size: Int
) -> AnyPublisher<AlarmListResponse, NetworkError>
}

class APIService: APIServiceType {
private let anonymousProvider: MoyaProvider<AnonymousAPI>
private let userProvider: MoyaProvider<UserAPI>
private let authProvider: MoyaProvider<AuthAPITarget>
private let subscriptionProvider: MoyaProvider<SubscriptionAPI>
private let alarmProvider: MoyaProvider<AlarmAPI>

init(userSession: UserSession = UserSession()) {
anonymousProvider = NetworkKit.provider(userSession: userSession)
#if DEBUG
// 개발 모드에서는 로깅 플러그인 추가
let logger = NetworkLoggerPlugin(configuration: .init(
logOptions: .verbose
))
userProvider = NetworkKit.provider(userSession: userSession, plugins: [logger])
authProvider = NetworkKit.provider(userSession: userSession, plugins: [logger])
subscriptionProvider = NetworkKit.provider(userSession: userSession, plugins: [logger])
alarmProvider = NetworkKit.provider(userSession: userSession, plugins: [logger])
#else
userProvider = NetworkKit.provider(userSession: userSession)
authProvider = NetworkKit.provider(userSession: userSession)
subscriptionProvider = NetworkKit.provider(userSession: userSession)
alarmProvider = NetworkKit.provider(userSession: userSession)
#endif
}

// MARK: - Generic Request

func request<T: Decodable>(_ target: some TargetType) -> AnyPublisher<T, NetworkError> {
Fail<T, NetworkError>(error: .requestFailed(NSError(domain: "NotImplemented", code: -1))).eraseToAnyPublisher()
Fail<T, NetworkError>(error: .requestFailed(NSError(domain: "NotImplemented", code: -1)))
.eraseToAnyPublisher()
}

func createAnonymous(deviceSecret: String) -> AnyPublisher<AnonymousUserResponse, NetworkError> {
anonymousProvider.requestPublisher(.createAnonymous(deviceSecret: deviceSecret))
// MARK: - User API

func registerAnonymous(deviceSecret: String) -> AnyPublisher<RegisterAnonymousResponse, NetworkError> {
userProvider.requestPublisher(.registerAnonymous(deviceSecret: deviceSecret))
.tryMap { response -> Data in
// 상태 코드 체크
guard (200 ... 299).contains(response.statusCode) else {
throw NetworkError.serverError(statusCode: response.statusCode)
}

// 🐛 디버깅: 서버 응답 출력
#if DEBUG
if let jsonString = String(data: response.data, encoding: .utf8) {
print("📥 서버 응답 (Raw JSON):")
print(jsonString)
}
#endif

return response.data
}
.decode(type: AnonymousUserResponse.self, decoder: JSONDecoder())
// JSON 데이터를 RegisterAnonymousResponse 구조체로 자동 변환
.decode(type: RegisterAnonymousResponse.self, decoder: JSONDecoder())
.mapError { error -> NetworkError in
// 🐛 디버깅: 디코딩 에러 상세 출력
#if DEBUG
if let decodingError = error as? DecodingError {
print("❌ 디코딩 에러 발생:")
switch decodingError {
case let .keyNotFound(key, context):
print(" - 키를 찾을 수 없음: \(key.stringValue)")
print(" - 경로: \(context.codingPath.map(\.stringValue).joined(separator: " -> "))")
print(" - 설명: \(context.debugDescription)")
case let .typeMismatch(type, context):
print(" - 타입 불일치: \(type)")
print(" - 경로: \(context.codingPath.map(\.stringValue).joined(separator: " -> "))")
case let .valueNotFound(type, context):
print(" - 값을 찾을 수 없음: \(type)")
print(" - 경로: \(context.codingPath.map(\.stringValue).joined(separator: " -> "))")
case let .dataCorrupted(context):
print(" - 데이터 손상")
print(" - 경로: \(context.codingPath.map(\.stringValue).joined(separator: " -> "))")
@unknown default:
print(" - 알 수 없는 디코딩 에러")
}
}
#endif

if let networkError = error as? NetworkError {
return networkError
} else if error is DecodingError {
Expand All @@ -39,4 +102,122 @@ class APIService: APIServiceType {
}
.eraseToAnyPublisher()
}

// MARK: - Subscription API

func getSubscriptions(
userId: String, deviceSecret: String, page: Int = 0, size: Int = 10
) -> AnyPublisher<SubscriptionListResponse, NetworkError> {
subscriptionProvider.requestPublisher(.getSubscriptions(
userId: userId,
deviceSecret: deviceSecret,
page: page,
size: size
))
.tryMap { response -> Data in
// 상태 코드 체크
guard (200 ... 299).contains(response.statusCode) else {
throw NetworkError.serverError(statusCode: response.statusCode)
}

// 🐛 디버깅: 서버 응답 출력
#if DEBUG
if let jsonString = String(data: response.data, encoding: .utf8) {
print("📥 구독 목록 응답 (Raw JSON):")
print(jsonString)
}
#endif

return response.data
}
.decode(type: SubscriptionListResponse.self, decoder: JSONDecoder())
.mapError { error -> NetworkError in
// 🐛 디버깅: 디코딩 에러 상세 출력
#if DEBUG
if let decodingError = error as? DecodingError {
print("❌ 디코딩 에러 발생:")
switch decodingError {
case let .keyNotFound(key, context):
print(" - 키를 찾을 수 없음: \(key.stringValue)")
print(" - 경로: \(context.codingPath.map(\.stringValue).joined(separator: " -> "))")
case let .typeMismatch(type, context):
print(" - 타입 불일치: \(type)")
print(" - 경로: \(context.codingPath.map(\.stringValue).joined(separator: " -> "))")
@unknown default:
print(" - 알 수 없는 디코딩 에러")
}
}
#endif

if let networkError = error as? NetworkError {
return networkError
} else if error is DecodingError {
return .decodingFailed(error)
} else {
return .requestFailed(error)
}
}
.eraseToAnyPublisher()
}

// MARK: - Alarm API

func getAlarms(
userId: String, deviceSecret: String, page: Int = 0, size: Int = 10
) -> AnyPublisher<AlarmListResponse, NetworkError> {
alarmProvider.requestPublisher(.getAlarms(
userId: userId,
deviceSecret: deviceSecret,
page: page,
size: size
))
.tryMap { response -> Data in
// 상태 코드 체크
guard (200 ... 299).contains(response.statusCode) else {
throw NetworkError.serverError(statusCode: response.statusCode)
}

// 🐛 디버깅: 서버 응답 출력
#if DEBUG
if let jsonString = String(data: response.data, encoding: .utf8) {
print("📥 알림 목록 응답 (Raw JSON):")
print(jsonString)
}
#endif

return response.data
}
.decode(type: AlarmListResponse.self, decoder: JSONDecoder())
.mapError { error -> NetworkError in
// 🐛 디버깅: 디코딩 에러 상세 출력
#if DEBUG
if let decodingError = error as? DecodingError {
print("❌ 디코딩 에러 발생:")
switch decodingError {
case let .keyNotFound(key, context):
print(" - 키를 찾을 수 없음: \(key.stringValue)")
print(" - 경로: \(context.codingPath.map(\.stringValue).joined(separator: " -> "))")
case let .typeMismatch(type, context):
print(" - 타입 불일치: \(type)")
print(" - 경로: \(context.codingPath.map(\.stringValue).joined(separator: " -> "))")
case let .valueNotFound(type, context):
print(" - 값을 찾을 수 없음: \(type)")
case let .dataCorrupted(context):
print(" - 데이터 손상")
@unknown default:
print(" - 알 수 없는 디코딩 에러")
}
}
#endif

if let networkError = error as? NetworkError {
return networkError
} else if error is DecodingError {
return .decodingFailed(error)
} else {
return .requestFailed(error)
}
}
.eraseToAnyPublisher()
}
}
Loading
Loading