Skip to content
Merged

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 0 additions & 10 deletions MovieBooking/Data/DTO/Response/MovieDTO.swift

This file was deleted.

43 changes: 43 additions & 0 deletions MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// MovieListResponseDTO.swift
// MovieBooking
//
// Created by 김민희 on 10/15/25.
//

import Foundation

struct MovieListResponseDTO: Decodable {
let page: Int
let results: [MovieDTO]
let totalPages: Int
let totalResults: Int
let dates: DatesDTO?
}

struct MovieDTO: Decodable {
let id: Int
let title: String
let overview: String
let posterPath: String?
let releaseDate: String
let voteAverage: Double
}

struct DatesDTO: Decodable {
let maximum: String
let minimum: String
}

extension MovieDTO {
func toDomain() -> Movie {
Movie(
id: id,
title: title,
overview: overview,
posterPath: posterPath ?? "",
releaseDate: releaseDate,
voteAverage: voteAverage
)
}
}
17 changes: 14 additions & 3 deletions MovieBooking/Data/DataSources/MovieDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation

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

struct DefaultMovieDataSource: MovieDataSource {
Expand All @@ -23,10 +24,13 @@ struct DefaultMovieDataSource: MovieDataSource {
) async throws -> MovieDetailResponseDTO {
try await provider.request(MovieTarget.movieDetail(id: id))
}
}

enum MovieTarget {
case movieDetail(id: String)
func movieList(
category: MovieCategory,
page: Int = 1
) async throws -> MovieListResponseDTO {
try await provider.request(MovieTarget.movieList(category: category, page: page))
}
}

extension MovieTarget: TargetType {
Expand All @@ -38,20 +42,27 @@ extension MovieTarget: TargetType {
switch self {
case .movieDetail(let id):
return "/\(id)"
case .movieList(let category, _):
return "/\(category.rawValue)"
}
}

var method: HTTPMethod {
switch self {
case .movieDetail:
return .get
case .movieList:
return .get
}
}

var parameters: RequestParameter? {
switch self {
case .movieDetail:
return nil

case .movieList(_ , let page):
return .query(["page": page])
}
}

Expand Down
23 changes: 23 additions & 0 deletions MovieBooking/Data/Repository/MockMovieRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// MockMovieRepository.swift
// MovieBooking
//
// Created by 김민희 on 10/15/25.
//

import Foundation

struct MockMovieRepository: MovieRepositoryProtocol {
func fetchUpcomingMovies() async throws -> [Movie] {
return Movie.mockData
}

func fetchPopularMovies() async throws -> [Movie] {
return Movie.mockData
}

func fetchNowPlayingMovies() async throws -> [Movie] {
try await Task.sleep(for: .seconds(1))
return Movie.mockData
}
}
26 changes: 26 additions & 0 deletions MovieBooking/Data/Repository/MovieRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,29 @@
// Created by 김민희 on 10/13/25.
//

import Foundation

struct MovieRepository: MovieRepositoryProtocol {
private let dataSource: MovieDataSource

init(dataSource: MovieDataSource = DefaultMovieDataSource()) {
self.dataSource = dataSource
}

func fetchMovies(for category: MovieCategory, page: Int = 1) async throws -> [Movie] {
let dto = try await dataSource.movieList(category: category, page: page)
return dto.results.map { $0.toDomain() }
}

func fetchNowPlayingMovies() async throws -> [Movie] {
try await fetchMovies(for: .nowPlaying)
}

func fetchUpcomingMovies() async throws -> [Movie] {
try await fetchMovies(for: .upcoming)
}

func fetchPopularMovies() async throws -> [Movie] {
try await fetchMovies(for: .popular)
}
}
15 changes: 15 additions & 0 deletions MovieBooking/Data/Request/MovieCategory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// MovieCategory.swift
// MovieBooking
//
// Created by 김민희 on 10/17/25.
//

import Foundation

enum MovieCategory: String {
case popular
case nowPlaying = "now_playing"
case topRated = "top_rated"
case upcoming
}
13 changes: 13 additions & 0 deletions MovieBooking/Data/Request/MovieTarget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// MovieTarget.swift
// MovieBooking
//
// Created by 김민희 on 10/17/25.
//

import Foundation

enum MovieTarget {
case movieDetail(id: String)
case movieList(category: MovieCategory, page: Int = 1)
}
35 changes: 35 additions & 0 deletions MovieBooking/Domain/Entity/Movie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,38 @@
// Created by 김민희 on 10/13/25.
//

import Foundation

public struct Movie: Identifiable, Equatable {
public let id: Int
public let title: String
public let overview: String
public let posterPath: String?
public let releaseDate: String
public let voteAverage: Double

public init(
id: Int,
title: String,
overview: String,
posterPath: String?,
releaseDate: String,
voteAverage: Double
) {
self.id = id
self.title = title
self.overview = overview
self.posterPath = posterPath
self.releaseDate = releaseDate
self.voteAverage = voteAverage
}
}


extension Movie {
static let mock1 = Movie(id: 1, title: "TCA 대모험", overview: "한 개발자의 TCA 입문기...", posterPath: "/path1.jpg", releaseDate: "2025-01-01", voteAverage: 2)
static let mock2 = Movie(id: 2, title: "클린 아키텍처의 비밀", overview: "레이어를 분리하며 벌어지는 미스터리...", posterPath: "/path2.jpg", releaseDate: "2025-01-02", voteAverage: 4)
static let mock3 = Movie(id: 3, title: "SwiftUI 애니메이션", overview: "뷰가 살아 움직인다!", posterPath: "/path3.jpg", releaseDate: "2025-01-03", voteAverage: 5)

static let mockData: [Movie] = [mock1, mock2, mock3]
}
21 changes: 21 additions & 0 deletions MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,24 @@
// Created by 김민희 on 10/13/25.
//

import Foundation
import Dependencies

protocol MovieRepositoryProtocol {
func fetchNowPlayingMovies() async throws -> [Movie]
func fetchUpcomingMovies() async throws -> [Movie]
func fetchPopularMovies() async throws -> [Movie]
}

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

extension DependencyValues {
var movieRepository: MovieRepositoryProtocol {
get { self[MovieRepositoryKey.self] }
set { self[MovieRepositoryKey.self] = newValue }
}
}
Comment on lines +11 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 좋은데요 ? repository도 해보고싶었는데 단일 레포라 가능하겠네요

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// CircularArrowButton.swift
// MovieBooking
//
// Created by 김민희 on 10/14/25.
//

import SwiftUI

enum ArrowDirection {
case left
case right
}

struct CircularArrowButton: View {
private let direction: ArrowDirection
private let action: () -> Void

init(direction: ArrowDirection, action: @escaping () -> Void) {
self.direction = direction
self.action = action
}

private var systemImageName: String {
switch self.direction {
case .left:
return "chevron.left"
case .right:
return "chevron.right"
}
}

var body: some View {
Button(action: action) {
ZStack {
Circle()
.stroke(.gray.opacity(0.4), lineWidth: 1)

Image(systemName: systemImageName)
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(.black)
}
.frame(width: 30, height: 30)
}
}
}
Loading