diff --git a/MovieBooking.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MovieBooking.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index eeb7cba..3d1398a 100644 --- a/MovieBooking.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/MovieBooking.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/supabase/supabase-swift.git", "state" : { - "revision" : "21425be5a493bb24bfde51808ccfa82a56111430", - "version" : "2.34.0" + "revision" : "d7fb6517eaff9cdcc105367a43068d6c0a49a5c9", + "version" : "2.35.0" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-asn1.git", "state" : { - "revision" : "f70225981241859eb4aa1a18a75531d26637c8cc", - "version" : "1.4.0" + "revision" : "40d25bbb2fc5b557a9aa8512210bded327c0f60d", + "version" : "1.5.0" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc", - "version" : "3.15.1" + "revision" : "bcd2b89f2a4446395830b82e4e192765edd71e18", + "version" : "4.0.0" } }, { @@ -112,7 +112,7 @@ { "identity" : "swift-dependencies", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies.git", + "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { "revision" : "a10f9feeb214bc72b5337b6ef6d5a029360db4cc", "version" : "1.10.0" diff --git a/MovieBooking/Data/DTO/Response/MovieDTO.swift b/MovieBooking/Data/DTO/Response/MovieDTO.swift deleted file mode 100644 index 1a78d6e..0000000 --- a/MovieBooking/Data/DTO/Response/MovieDTO.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// MovieDTO.swift -// MovieBooking -// -// Created by 김민희 on 10/13/25. -// - -struct MovieDTO: Decodable { - -} diff --git a/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift b/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift new file mode 100644 index 0000000..38ea35d --- /dev/null +++ b/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift @@ -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 + ) + } +} diff --git a/MovieBooking/Data/DataSources/MovieDataSource.swift b/MovieBooking/Data/DataSources/MovieDataSource.swift index 399ab55..b6a2635 100644 --- a/MovieBooking/Data/DataSources/MovieDataSource.swift +++ b/MovieBooking/Data/DataSources/MovieDataSource.swift @@ -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 { @@ -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 { @@ -38,6 +42,8 @@ extension MovieTarget: TargetType { switch self { case .movieDetail(let id): return "/\(id)" + case .movieList(let category, _): + return "/\(category.rawValue)" } } @@ -45,6 +51,8 @@ extension MovieTarget: TargetType { switch self { case .movieDetail: return .get + case .movieList: + return .get } } @@ -52,6 +60,9 @@ extension MovieTarget: TargetType { switch self { case .movieDetail: return nil + + case .movieList(_ , let page): + return .query(["page": page]) } } diff --git a/MovieBooking/Data/Repository/MockMovieRepository.swift b/MovieBooking/Data/Repository/MockMovieRepository.swift new file mode 100644 index 0000000..d544806 --- /dev/null +++ b/MovieBooking/Data/Repository/MockMovieRepository.swift @@ -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 + } +} diff --git a/MovieBooking/Data/Repository/MovieRepository.swift b/MovieBooking/Data/Repository/MovieRepository.swift index 588bdcd..09d6dea 100644 --- a/MovieBooking/Data/Repository/MovieRepository.swift +++ b/MovieBooking/Data/Repository/MovieRepository.swift @@ -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) + } +} diff --git a/MovieBooking/Data/Request/MovieCategory.swift b/MovieBooking/Data/Request/MovieCategory.swift new file mode 100644 index 0000000..202525a --- /dev/null +++ b/MovieBooking/Data/Request/MovieCategory.swift @@ -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 +} diff --git a/MovieBooking/Data/Request/MovieTarget.swift b/MovieBooking/Data/Request/MovieTarget.swift new file mode 100644 index 0000000..0c05005 --- /dev/null +++ b/MovieBooking/Data/Request/MovieTarget.swift @@ -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) +} diff --git a/MovieBooking/Domain/Entity/Movie.swift b/MovieBooking/Domain/Entity/Movie.swift index c86367c..2045266 100644 --- a/MovieBooking/Domain/Entity/Movie.swift +++ b/MovieBooking/Domain/Entity/Movie.swift @@ -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] +} diff --git a/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift b/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift index 1d125ac..04050c4 100644 --- a/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift +++ b/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift @@ -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 } + } +} diff --git a/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift b/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift new file mode 100644 index 0000000..fc9ea1e --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift @@ -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) + } + } +} diff --git a/MovieBooking/Feature/MovieList/Components/MovieCardView.swift b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift new file mode 100644 index 0000000..6281774 --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift @@ -0,0 +1,73 @@ +// +// MovieCardView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct MovieCardView: View { + private let movieTitle: String + private let movieRating: Int + private let posterPath: String? + + init(movieTitle: String, movieRating: Int, posterPath: String?) { + self.movieTitle = movieTitle + self.movieRating = movieRating + self.posterPath = posterPath + } + + private var imageURL: URL? { + guard let path = posterPath else { return nil } + return URL(string: "https://image.tmdb.org/t/p/w500\(path)") + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + AsyncImage(url: imageURL) { phase in + switch phase { + case .empty: + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.2)) + ProgressView() + } + .frame(width: 150, height: 200) + + case .success(let image): + image + .resizable() + .scaledToFit() + .frame(width: 150, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(radius: 3, y: 3) + + case .failure: + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.3)) + Image(systemName: "film") + .font(.system(size: 40)) + .foregroundColor(.gray) + } + .frame(width: 150, height: 200) + + @unknown default: + EmptyView() + } + } + + Text(movieTitle) + .font(.system(size: 16)) + .frame(width: 150, alignment: .leading) + .lineLimit(1) + + StarRatingView(rating: movieRating) + } + } +} + +#Preview { + MovieCardView(movieTitle: "mvTitle", movieRating: 4, posterPath: nil) +} diff --git a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift new file mode 100644 index 0000000..e6a6b64 --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift @@ -0,0 +1,83 @@ +// +// MovieTypeSectionView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct MovieTypeSectionView: View { + private let cardCount: Int + private let headerText: String + private let movies: [Movie] + @State private var currentIndex: Int = 0 + + init(cardCount: Int, headerText: String, movies: [Movie], currentIndex: Int = 0) { + self.cardCount = cardCount + self.headerText = headerText + self.movies = movies + self.currentIndex = currentIndex + } + + var body: some View { + VStack(spacing: 10) { + HeaderView( + headerText: headerText, + onLeftTapped: { + if currentIndex > 0 { + currentIndex -= 1 + } + }, + onRightTapped: { + if currentIndex < movies.count - 1 { + currentIndex += 1 + } + } + ) + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 15) { + ForEach(movies) { movie in + MovieCardView( + movieTitle: movie.title, + movieRating: Int(movie.voteAverage / 2), + posterPath: movie.posterPath + ) + .id(movie.id) + } + } + } + .onChange(of: currentIndex) { newIndex in + withAnimation { + proxy.scrollTo(movies[newIndex].id, anchor: .leading) + } + } + } + } + .padding(.horizontal, 20) + } +} + +struct HeaderView: View { + let headerText: String + let onLeftTapped: () -> Void + let onRightTapped: () -> Void + + var body: some View { + HStack(spacing: 10) { + Text(headerText) + .font(.system(size: 16)) + + Spacer() + + CircularArrowButton(direction: .left, action: onLeftTapped) + + CircularArrowButton(direction: .right, action: onRightTapped) + } + } +} + +//#Preview { +// MovieTypeSectionView(cardCount: 5, headerText: "Now Showing", movie: <#[Movie]#>) +//} diff --git a/MovieBooking/Feature/MovieList/Components/StarRatingView.swift b/MovieBooking/Feature/MovieList/Components/StarRatingView.swift new file mode 100644 index 0000000..f5ab388 --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/StarRatingView.swift @@ -0,0 +1,28 @@ +// +// StarRatingView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct StarRatingView: View { + private let rating: Int + + init(rating: Int) { + self.rating = rating + } + + var body: some View { + HStack(spacing: 4) { + ForEach(1...5, id: \.self) { index in + Image(systemName: "star.fill") + .resizable() + .scaledToFit() + .frame(width: 12, height: 12) + .foregroundColor(index <= rating ? .yellow : .gray) + } + } + } +} diff --git a/MovieBooking/Feature/MovieList/MovieListFeature.swift b/MovieBooking/Feature/MovieList/MovieListFeature.swift new file mode 100644 index 0000000..760e986 --- /dev/null +++ b/MovieBooking/Feature/MovieList/MovieListFeature.swift @@ -0,0 +1,134 @@ +// +// MovieListFeature.swift +// MovieBooking +// +// Created by 김민희 on 10/15/25. +// + +import Foundation +import ComposableArchitecture +import SwiftUI + +@Reducer +struct MovieListFeature { + @Dependency(\.movieRepository) var movieRepository + + @ObservableState + public struct State { + var nowPlayingMovies: [Movie] = [] + var upcomingMovies: [Movie] = [] + var popularMovies: [Movie] = [] + var isLoading = false + @Presents var alert: AlertState? + } + + enum Action { + case onAppear + case fetchMovie + case fetchNowPlayingResponse(Result<[Movie], Error>) + case fetchUpcomingResponse(Result<[Movie], Error>) + case fetchPopularResponse(Result<[Movie], Error>) + case selectMovie + case alert(PresentationAction) + + enum Alert: Equatable { + case retry + } + } + + var body: some Reducer { + Reduce { state, action in + switch action { + case .onAppear: + return .send(.fetchMovie) + + case .fetchMovie: + state.isLoading = true + state.alert = nil + return .run { send in + await withThrowingTaskGroup(of: Void.self) { group in + // Now Playing + group.addTask { + do { + let movies = try await movieRepository.fetchNowPlayingMovies() + await send(.fetchNowPlayingResponse(.success(movies))) + } catch { + await send(.fetchNowPlayingResponse(.failure(error))) + } + } + // Upcoming + group.addTask { + do { + let movies = try await movieRepository.fetchUpcomingMovies() + await send(.fetchUpcomingResponse(.success(movies))) + } catch { + await send(.fetchUpcomingResponse(.failure(error))) + } + } + // Popular + group.addTask { + do { + let movies = try await movieRepository.fetchPopularMovies() + await send(.fetchPopularResponse(.success(movies))) + } catch { + await send(.fetchPopularResponse(.failure(error))) + } + } + + do { + try await group.waitForAll() + } catch { + print("⚠️ 일부 영화 목록 로드 실패: \(error)") + } + } + } + + case let .fetchNowPlayingResponse(.success(movies)): + state.nowPlayingMovies = movies + state.isLoading = false + return .none + + case let .fetchUpcomingResponse(.success(movies)): + state.upcomingMovies = movies + state.isLoading = false + return .none + + case let .fetchPopularResponse(.success(movies)): + state.popularMovies = movies + state.isLoading = false + return .none + + case let .fetchNowPlayingResponse(.failure(error)), + let .fetchUpcomingResponse(.failure(error)), + let .fetchPopularResponse(.failure(error)): + let networkError = (error as? NetworkError) ?? .unknown(error) + let message = networkError.errorDescription ?? "잠시 후 다시 시도해 주세요" + print("❌ 영화 가져오기 실패: \(networkError)") + state.isLoading = false + state.alert = AlertState { + TextState("영화 목록을 불러오지 못했습니다") + } actions: { + ButtonState(action: .send(.retry)) { + TextState("재시도") + } + ButtonState(role: .cancel) { + TextState("확인") + } + } message: { + TextState(message) + } + return .none + + case .selectMovie: + //TODO: 상세로 넘어감 + return .none + + case .alert(.presented(.retry)): + return .send(.fetchMovie) + + case .alert: + return .none + } + } + } +} diff --git a/MovieBooking/Feature/MovieList/MovieListView.swift b/MovieBooking/Feature/MovieList/MovieListView.swift new file mode 100644 index 0000000..cc2e110 --- /dev/null +++ b/MovieBooking/Feature/MovieList/MovieListView.swift @@ -0,0 +1,33 @@ +// +// MovieListView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI +import ComposableArchitecture + +struct MovieListView: View { + @Perception.Bindable var store: StoreOf + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 20) { + MovieTypeSectionView(cardCount: 5, headerText: "Now Showing", movies: store.nowPlayingMovies) + MovieTypeSectionView(cardCount: 4, headerText: "Coming Soon", movies: store.upcomingMovies) + MovieTypeSectionView(cardCount: 3, headerText: "인기 영화", movies: store.popularMovies) + } + } + .task { + await store.send(.onAppear).finish() + } + .alert($store.scope(state: \.alert, action: \.alert)) + } +} + +//#Preview { +// MovieListView(store: Store(initialState: MovieListFeature.State(movies: Movie.mockData)) { +// MovieListFeature() +// }) +//} diff --git a/MovieBooking/Feature/Search/Components/EmptySearchView.swift b/MovieBooking/Feature/Search/Components/EmptySearchView.swift new file mode 100644 index 0000000..bf951ae --- /dev/null +++ b/MovieBooking/Feature/Search/Components/EmptySearchView.swift @@ -0,0 +1,34 @@ +// +// EmptySearchView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct EmptySearchView: View { + var body: some View { + VStack(spacing: 16) { + Image(systemName: "magnifyingglass") + .font(.system(size: 40)) + .foregroundColor(.purple) + .padding(20) + .background(Color.purple.opacity(0.1)) + .clipShape(Circle()) + + Text("영화를 검색해보세요") + .font(.headline) + .foregroundColor(.black) + + Text("보고싶은 영화를 찾아보세요") + .font(.subheadline) + .foregroundColor(.gray) + } + .multilineTextAlignment(.center) + } +} + +#Preview { + EmptySearchView() +} diff --git a/MovieBooking/Feature/Search/Components/SearchBar.swift b/MovieBooking/Feature/Search/Components/SearchBar.swift new file mode 100644 index 0000000..61d1ee8 --- /dev/null +++ b/MovieBooking/Feature/Search/Components/SearchBar.swift @@ -0,0 +1,48 @@ +// +// SearchBar.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct SearchBar: View { + @Binding var text: String + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 30) + .stroke(.gray.opacity(0.4), lineWidth: 1) + + HStack(spacing: 10) { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + .font(.system(size: 18, weight: .semibold)) + .padding(.leading, 8) + + TextField("영화 제목을 검색하세요", text: $text) + .font(.system(size: 18, weight: .semibold)) + .frame(maxWidth: .infinity) + + if !text.isEmpty { + Button { + self.text = "" + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(.gray.opacity(0.6)) + .font(.system(size: 18, weight: .semibold)) + .padding(.trailing, 15) + } + } + } + .padding(.horizontal, 10) + } + .frame(height: 50) + } +} + +#Preview { + @State var text = "" + SearchBar(text: $text) +} diff --git a/MovieBooking/Feature/Search/Components/SearchView.swift b/MovieBooking/Feature/Search/Components/SearchView.swift new file mode 100644 index 0000000..049caf6 --- /dev/null +++ b/MovieBooking/Feature/Search/Components/SearchView.swift @@ -0,0 +1,36 @@ +// +// SearchView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct SearchView: View { + let movies: [Movie] + let columns: [GridItem] = [ + GridItem(.flexible()), + GridItem(.flexible()) + ] + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + Text("검색 결과 \(movies.count)개") + .font(.system(size: 16)) + .frame(alignment: .leading) + + ScrollView { + LazyVGrid(columns: columns, spacing: 20) { + ForEach(movies) { movie in + MovieCardView(movieTitle: movie.title, movieRating: Int(movie.voteAverage / 2), posterPath: movie.posterPath) + } + } + } + } + } +} + +//#Preview { +// SearchView() +//} diff --git a/MovieBooking/Feature/Search/MovieSearchView.swift b/MovieBooking/Feature/Search/MovieSearchView.swift new file mode 100644 index 0000000..2531548 --- /dev/null +++ b/MovieBooking/Feature/Search/MovieSearchView.swift @@ -0,0 +1,25 @@ +// +// MovieSearchView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct MovieSearchView: View { + @State private var searchText = "" + + var body: some View { + VStack(spacing: 20) { + SearchBar(text: $searchText) + + SearchView(movies: Movie.mockData) + } + .padding(.horizontal, 20) + } +} + +#Preview { + MovieSearchView() +}