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 MovieBooking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
Data/DataSources/APIConfiguration.swift,
Data/DataSources/MovieDataSource.swift,
Data/DTO/Response/MovieDetailResponseDTO.swift,
Domain/Entity/Error/Movie/MovieDetailError.swift,
Feature/MovieDetail/MovieDetailFeature.swift,
NetworkService/Core/ContentType.swift,
NetworkService/Core/HTTPHeader.swift,
NetworkService/Core/HTTPHeaders.swift,
Expand Down
12 changes: 12 additions & 0 deletions MovieBooking/Data/DTO/Request/MovieDetailRequestDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// MovieDetailRequestDTO.swift
// MovieBooking
//
// Created by 홍석현 on 10/17/25.
//

import Foundation

struct MovieDetailRequestDTO: Encodable {
private let language: String = "ko"
}
48 changes: 33 additions & 15 deletions MovieBooking/Data/DTO/Response/MovieDetailResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,39 @@
import Foundation

struct MovieDetailResponseDTO: Decodable {
let id: Int
let title: String
let originalTitle: String
let overview: String?
let posterPath: String?
let backdropPath: String?
let releaseDate: String
let runtime: Int?
let genres: [Genre]
let voteAverage: Double
let voteCount: Int
let popularity: Double
let id: Int
let title: String
let originalTitle: String
let overview: String?
let posterPath: String?
let backdropPath: String?
let releaseDate: String
let runtime: Int
let genres: [GenreDTO]
let voteAverage: Double
let voteCount: Int
let popularity: Double
}

struct Genre: Decodable {
let id: Int
let name: String
struct GenreDTO: Decodable {
let id: Int
let name: String

func toDomain() -> Genre {
return Genre(id: String(self.id), name: self.name)
}
}

extension MovieDetailResponseDTO {
func toDomain() -> MovieDetail {
return MovieDetail(
title: self.title,
genres: self.genres.map { $0.toDomain() }, // 배열 전체 매핑
releaseDate: releaseDate,
runningTime: runtime * 60, // minutes to seconds
rating: voteAverage,
posterPath: posterPath,
summary: overview ?? "No overview available."
)
}
}
13 changes: 7 additions & 6 deletions MovieBooking/Data/DataSources/MovieDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

protocol MovieDataSource {
func movieDetail(_ id: String) async throws -> MovieDetailResponseDTO
func movieDetail(_ id: String, _ request: MovieDetailRequestDTO) async throws -> MovieDetailResponseDTO
func movieList(category: MovieCategory, page: Int) async throws -> MovieListResponseDTO
}

Expand All @@ -20,9 +20,10 @@ struct DefaultMovieDataSource: MovieDataSource {
}

func movieDetail(
_ id: String
_ id: String,
_ request: MovieDetailRequestDTO = MovieDetailRequestDTO()
) async throws -> MovieDetailResponseDTO {
try await provider.request(MovieTarget.movieDetail(id: id))
try await provider.request(MovieTarget.movieDetail(id: id, request: request))
}

func movieList(
Expand All @@ -40,7 +41,7 @@ extension MovieTarget: TargetType {

var path: String {
switch self {
case .movieDetail(let id):
case .movieDetail(let id, _):
return "/\(id)"
case .movieList(let category, _):
return "/\(category.rawValue)"
Expand All @@ -58,8 +59,8 @@ extension MovieTarget: TargetType {

var parameters: RequestParameter? {
switch self {
case .movieDetail:
return nil
case .movieDetail(_, let request):
return .query(request)

case .movieList(_ , let page):
return .query(["page": page])
Expand Down
4 changes: 4 additions & 0 deletions MovieBooking/Data/Repository/Movie/MockMovieRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ struct MockMovieRepository: MovieRepositoryProtocol {
try await Task.sleep(for: .seconds(1))
return Movie.mockData
}

func fetchMovieDetail(id: String) async throws -> MovieDetail {
throw NetworkError.httpError(statusCode: 404, response: nil, data: nil)
}
}
4 changes: 4 additions & 0 deletions MovieBooking/Data/Repository/Movie/MovieRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ struct MovieRepository: MovieRepositoryProtocol {
func fetchPopularMovies() async throws -> [Movie] {
try await fetchMovies(for: .popular)
}

func fetchMovieDetail(id: String) async throws -> MovieDetail {
try await dataSource.movieDetail(id, MovieDetailRequestDTO()).toDomain()
}
}
4 changes: 2 additions & 2 deletions MovieBooking/Data/Request/MovieTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
import Foundation

enum MovieTarget {
case movieDetail(id: String)
case movieList(category: MovieCategory, page: Int = 1)
case movieDetail(id: String, request: MovieDetailRequestDTO)
case movieList(category: MovieCategory, page: Int = 1)
}
68 changes: 68 additions & 0 deletions MovieBooking/Domain/Entity/Error/Movie/MovieDetailError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// MovieDetailError.swift
// MovieBooking
//
// Created by 홍석현 on 10/17/25.
//

import Foundation

enum MovieDetailError: Error {
case networkError(NetworkError)
case movieNotFound
case unknown(Error)

init(from error: Error) {
if let networkError = error as? NetworkError {
switch networkError {
case .httpError(let statusCode, _, _) where statusCode == 404:
self = .movieNotFound
default:
self = .networkError(networkError)
}
} else {
self = .unknown(error)
}
}
}

extension MovieDetailError: LocalizedError {
var errorDescription: String? {
switch self {
case .networkError(let networkError):
return networkError.errorDescription
case .movieNotFound:
return "영화 정보를 찾을 수 없습니다"
case .unknown(let error):
return "알 수 없는 오류: \(error.localizedDescription)"
}
}

var title: String {
switch self {
case .networkError(let networkError):
switch networkError {
case .invalidURL, .invalidResponse:
return "잘못된 요청"
case .noData:
return "데이터 없음"
case .httpError(let statusCode, _, _):
if statusCode >= 500 {
return "서버 오류"
} else {
return "네트워크 오류"
}
case .decodingError:
return "데이터 오류"
case .encodingError:
return "요청 오류"
case .unknown:
return "알 수 없는 오류"
}
case .movieNotFound:
return "영화 없음"
case .unknown:
return "오류 발생"
}
}
}
27 changes: 19 additions & 8 deletions MovieBooking/Domain/Entity/MovieDetail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,51 @@ import Foundation

struct MovieDetail {
let title: String
let genres: String
let genres: [Genre] // 배열로 변경
let releaseDate: Date?
let runningTime: Int
let rating: Double
let posterPath: String?
let summary: String

let ticketPrice: Int // 티켓 가격

init(
title: String,
genres: String,
genres: [Genre], // 배열로 변경
releaseDate: String,
runningTime: Int,
rating: Double,
posterPath: String?,
summary: String
summary: String,
ticketPrice: Int = 13000 // 기본값 13,000원
) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd."
dateFormatter.dateFormat = "yyyy-MM-dd"

self.title = title
self.genres = genres
self.releaseDate = dateFormatter.date(from: releaseDate)
self.runningTime = runningTime
self.rating = rating
self.posterPath = posterPath
self.summary = summary
self.ticketPrice = ticketPrice
}
}

struct Genre {
let id: String
let name: String
}

extension MovieDetail {
static let mockData: MovieDetail = MovieDetail(
title: "The Shawshank Redemption",
genres: "Drama",
releaseDate: "1994-9-23.",
genres: [
Genre(id: "18", name: "드라마"),
Genre(id: "80", name: "범죄")
],
releaseDate: "1994-09-23",
runningTime: 142 * 60,
rating: 8.7,
posterPath: "/bUrReoZFLGti6ehkBW0xw8f12MT.jpg",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ protocol MovieRepositoryProtocol {
func fetchNowPlayingMovies() async throws -> [Movie]
func fetchUpcomingMovies() async throws -> [Movie]
func fetchPopularMovies() async throws -> [Movie]
func fetchMovieDetail(id: String) async throws -> MovieDetail
}

private enum MovieRepositoryKey: DependencyKey {
static let liveValue: any MovieRepositoryProtocol = MovieRepository()
static let previewValue: any MovieRepositoryProtocol = MockMovieRepository()
static let previewValue: any MovieRepositoryProtocol = MovieRepository()
static let testValue: any MovieRepositoryProtocol = MockMovieRepository()
}

Expand Down
44 changes: 44 additions & 0 deletions MovieBooking/Domain/UseCase/Movie/FetchMovieDetailUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// FetchMovieDetailUseCase.swift
// MovieBooking
//
// Created by 홍석현 on 10/17/25.
//

import Foundation
import Dependencies

protocol FetchMovieDetailUseCaseProtocol {
func execute(_ id: String) async throws -> MovieDetail
}

struct FetchMovieDetailUseCase: FetchMovieDetailUseCaseProtocol {
@Dependency(\.movieRepository) var repository: MovieRepositoryProtocol

func execute(_ id: String) async throws -> MovieDetail {
do {
return try await repository.fetchMovieDetail(id: id)
} catch {
throw MovieDetailError(from: error)
}
}
}

private enum FetchMovieDetailUseCaseKey: DependencyKey {
static let liveValue: any FetchMovieDetailUseCaseProtocol = FetchMovieDetailUseCase()
static let previewValue: any FetchMovieDetailUseCaseProtocol = FetchMovieDetailUseCase()
static let testValue: any FetchMovieDetailUseCaseProtocol = MockFetchMovieDetailUseCase()
}

extension DependencyValues {
var fetchMovieDetailUseCase: FetchMovieDetailUseCaseProtocol {
get { self[FetchMovieDetailUseCaseKey.self] }
set { self[FetchMovieDetailUseCaseKey.self] = newValue }
}
}

struct MockFetchMovieDetailUseCase: FetchMovieDetailUseCaseProtocol {
func execute(_ id: String) async throws -> MovieDetail {
return MovieDetail.mockData
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//
// StarRatingView.swift
// DetailStarRatingView.swift
// MovieBooking
//
// Created by 홍석현 on 10/17/25.
//

import SwiftUI

struct StarRatingView: View {
struct DetailStarRatingView: View {
private let rating: Double // 0.0 ~ 10.0 범위
private let maxStars: Int = 5

Expand Down Expand Up @@ -72,15 +72,15 @@ struct StarRatingView: View {

#Preview {
VStack(spacing: 20) {
StarRatingView(rating: 8.7)
DetailStarRatingView(rating: 8.7)

StarRatingView(rating: 7.5)
DetailStarRatingView(rating: 7.5)

StarRatingView(rating: 9.2)
DetailStarRatingView(rating: 9.2)

StarRatingView(rating: 5.0)
DetailStarRatingView(rating: 5.0)

StarRatingView(rating: 10.0)
DetailStarRatingView(rating: 10.0)
}
.padding()
}
Loading