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
15 changes: 15 additions & 0 deletions AppleStoreProductKiosk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,22 @@
C8F725F52E790FA100C2A1DE /* AppleStoreProductKioskUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppleStoreProductKioskUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
7F41C5542E798756007C4017 /* Exceptions for "AppleStoreProductKiosk" folder in "AppleStoreProductKiosk" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Resources/Info.plist,
);
target = C8F725DD2E790F9F00C2A1DE /* AppleStoreProductKiosk */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
C8F725E02E790F9F00C2A1DE /* AppleStoreProductKiosk */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
7F41C5542E798756007C4017 /* Exceptions for "AppleStoreProductKiosk" folder in "AppleStoreProductKiosk" target */,
);
path = AppleStoreProductKiosk;
sourceTree = "<group>";
};
Expand Down Expand Up @@ -415,6 +428,7 @@
DEVELOPMENT_TEAM = N94CS4N6VR;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AppleStoreProductKiosk/Resources/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down Expand Up @@ -448,6 +462,7 @@
DEVELOPMENT_TEAM = N94CS4N6VR;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AppleStoreProductKiosk/Resources/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down
24 changes: 24 additions & 0 deletions AppleStoreProductKiosk/Network/API/APIDomain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// APIDomain.swift
// AppleStoreProductKiosk
//
// Created by Wonji Suh on 9/16/25.
//

import Foundation

// MARK: - API Domain Types

/// API 도메인 타입 정의
///
/// 각 도메인별 basePath를 관리하는 단일 책임을 가집니다.
enum APIDomain {
case kiosk

var basePath: String {
switch self {
case .kiosk:
return "/api/apple-store-kiosk"
}
}
}
35 changes: 35 additions & 0 deletions AppleStoreProductKiosk/Network/API/APIEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// APIEndpoint.swift
// AppleStoreProductKiosk
//
// Created by Wonji Suh on 9/16/25.
//

import Foundation

// MARK: - APIEndpoint Protocol

/// API 엔드포인트의 공통 속성을 정의하는 프로토콜
protocol APIEndpoint {
var baseURL: String { get }
var basePath: String { get }
var path: String { get }
var fullPath: String { get }
var fullURL: String { get }
}

// MARK: - APIEndpoint Default Implementation

extension APIEndpoint {
var baseURL: String {
"https://applestoreproductkiosk.free.beeceptor.com"
}

var fullPath: String {
basePath + path
}

var fullURL: String {
baseURL + fullPath
}
}
34 changes: 34 additions & 0 deletions AppleStoreProductKiosk/Network/API/KioskProductAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// KioskProductAPI.swift
// AppleStoreProductKiosk
//
// Created by Wonji Suh on 9/16/25.
//

import Foundation

// MARK: - KioskAPI Namespace

/// Kiosk API의 네임스페이스
///
/// Kiosk 관련 API 엔드포인트만을 관리하는 단일 책임을 가집니다.
enum KioskAPI {

// MARK: - Product API

/// 상품 관련 API 엔드포인트
enum Product: APIEndpoint {
case list

var basePath: String {
return APIDomain.kiosk.basePath
}

var path: String {
switch self {
case .list:
return "/products"
}
}
}
}
21 changes: 21 additions & 0 deletions AppleStoreProductKiosk/Network/Core/Data/Extension+Data.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Extension+Data.swift
// AppleStoreProductKiosk
//
// Created by Wonji Suh on 9/16/25.
//

import Foundation

/// Data 타입에 대한 JSON 디코딩 확장입니다.
extension Data {
/// Data를 지정한 Decodable 타입으로 디코딩합니다.
///
/// - Parameter type: 디코딩할 타입
/// - Returns: 디코딩된 객체
/// - Throws: 디코딩 실패 시 에러 발생
func decoded<T: Decodable>(as type: T.Type) throws -> T {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: self)
}
}
43 changes: 43 additions & 0 deletions AppleStoreProductKiosk/Network/Core/Errors/DataError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// DataError.swift
// AppleStoreProductKiosk
//
// Created by Wonji Suh on 9/16/25.
//

import Foundation

/// 네트워크 및 데이터 처리 과정에서 발생할 수 있는 오류를 정의한 enum입니다.
// NOTE: Moved to Network/Core/Errors to avoid Infra -> Data reverse dependency.
enum DataError: Error, LocalizedError, Sendable {
/// 데이터가 없음
case noData

/// 커스텀 에러 메시지
case customError(String)

/// 처리하지 않은 HTTP 상태 코드
case unhandledStatusCode(Int)

/// HTTP 응답 오류 (응답 객체, 에러 메시지)
case httpResponseError(HTTPURLResponse, String)

/// 디코딩 에러
case decodingError(Error)

/// 사용자에게 표시할 에러 메시지
var errorDescription: String? {
switch self {
case .noData:
return "데이터를 받지 못했습니다"
case .customError(let message):
return message
case .unhandledStatusCode(let code):
return "처리되지 않은 상태 코드: \(code)"
case .httpResponseError(let response, let message):
return "HTTP \(response.statusCode): \(message)"
case .decodingError(let error):
return "디코딩 실패: \(error.localizedDescription)"
}
}
}
113 changes: 113 additions & 0 deletions AppleStoreProductKiosk/Network/Core/Logger/URLSessionLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// URLSessionLogger.swift
// AppleStoreProductKiosk
//
// Created by Wonji Suh on 9/16/25.
//

import Foundation
import LogMacro

/// URLSession 네트워크 요청/응답을 콘솔에 로그로 출력하는 싱글턴 유틸리티입니다.
///
/// - DEBUG 모드에서만 동작하며, 요청/응답의 메서드, URL, 헤더, 바디, 에러 등을 보기 좋게 출력합니다.
final class URLSessionLogger {
/// 싱글턴 인스턴스
static let shared = URLSessionLogger()

/// 외부에서 직접 생성하지 못하도록 private init
private init() {}

// MARK: - 요청 로그

/// 네트워크 요청 정보를 콘솔에 출력합니다.
/// - Parameter request: 로깅할 URLRequest
@MainActor
func logRequest(_ request: URLRequest) {
#if DEBUG
Log.debug("⎡--------------------- REQUEST ---------------------⎤")

// HTTP 메서드
if let method = request.httpMethod {
Log.network("[Method]", method)
}

// 요청 URL
if let url = request.url?.absoluteString {
Log.network("[URL]", url)
}

// 헤더
if let headers = request.allHTTPHeaderFields {
Log.network("[Headers]")
for (key, value) in headers {
Log.network(" \(key): \(value)")
}
}

// 바디
if let body = request.httpBody,
let bodyString = String(data: body, encoding: .utf8) {
Log.network("[Body]")
Log.network(" \(bodyString)")
}

Log.network("⎣------------------ END REQUEST --------------------⎦")
#endif
}

// MARK: - 응답 로그

/// 네트워크 응답 정보를 콘솔에 출력합니다.
/// - Parameters:
/// - data: 응답 데이터
/// - response: URLResponse 객체
/// - error: 네트워크 에러
@MainActor
func logResponse(data: Data?, response: URLResponse?, error: Error?) {
#if DEBUG
Log.network("⎡--------------------- RESPONSE --------------------⎤")

// 응답 URL
if let url = response?.url?.absoluteString {
Log.network("[URL]", url)
}

// HTTP 상태 코드 및 헤더
if let httpResponse = response as? HTTPURLResponse {
Log.network("[Status Code] \(httpResponse.statusCode)")
Log.network("[Response Headers]")
for (key, value) in httpResponse.allHeaderFields {
Log.network(" \(key): \(value)")
}
}

// 바디 (JSON pretty print 시도, 실패 시 raw 출력)
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
let prettyData = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted])
if let prettyString = String(data: prettyData, encoding: .utf8) {
Log.network("[Body (Map)]")
Log.network(prettyString)
}
} catch {
// JSON 디코딩 실패 시 원본 문자열 출력
if let rawString = String(data: data, encoding: .utf8) {
Log.network("[Body (Raw)]")
Log.network(rawString)
} else {
Log.network("[Body] Cannot decode data")
}
}
}

// 에러
if let error = error {
Log.network("[Error] \(error.localizedDescription)")
}

Log.network("⎣------------------ END RESPONSE -------------------⎦")
#endif
}
}
21 changes: 21 additions & 0 deletions AppleStoreProductKiosk/Network/Core/Method/HTTPMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// HTTPMethod.swift
// AppleStoreProductKiosk
//
// Created by Wonji Suh on 9/16/25.
//

import Foundation

/// HTTP 요청에서 사용되는 메서드 타입을 정의한 enum입니다.
enum HTTPMethod: String {
/// GET: 데이터 조회
case get = "GET"
/// POST: 데이터 생성
case post = "POST"
/// PUT: 데이터 전체 수정
case put = "PUT"
/// DELETE: 데이터 삭제
case delete = "DELETE"
}

Loading