From 3510c2ccd8ecdf2f66586fe1b7bf57de5a4c5928 Mon Sep 17 00:00:00 2001 From: minneee Date: Tue, 14 Oct 2025 15:22:02 +0900 Subject: [PATCH 01/11] =?UTF-8?q?[feat]=20=EC=98=81=ED=99=94=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/CircularArrowButton.swift | 41 +++++++++++++++ .../MovieList/Components/MovieCardView.swift | 33 ++++++++++++ .../Components/MovieTypeSectionView.swift | 52 +++++++++++++++++++ .../MovieList/Components/StarRatingView.swift | 24 +++++++++ .../Feature/MovieList/MovieListView.swift | 24 +++++++++ 5 files changed, 174 insertions(+) create mode 100644 MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift create mode 100644 MovieBooking/Feature/MovieList/Components/MovieCardView.swift create mode 100644 MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift create mode 100644 MovieBooking/Feature/MovieList/Components/StarRatingView.swift create mode 100644 MovieBooking/Feature/MovieList/MovieListView.swift diff --git a/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift b/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift new file mode 100644 index 0000000..00286fa --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift @@ -0,0 +1,41 @@ +// +// CircularArrowButton.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +enum ArrowDirection { + case left + case right +} + +struct CircularArrowButton: View { + let direction: ArrowDirection + let action: () -> Void + + 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..d4cb544 --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift @@ -0,0 +1,33 @@ +// +// MovieCardView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct MovieCardView: View { + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Image("movie") + .resizable() + .scaledToFit() + .frame(width: 150, height: 200) + .background(.cyan) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(radius: 3, y: 3) + + Text("Movie Title") + .font(.system(size: 16)) + .frame(width: 150, alignment: .leading) + .lineLimit(1) + + StarRatingView(rating: 3) + } + } +} + +#Preview { + MovieCardView() +} diff --git a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift new file mode 100644 index 0000000..6cc7e98 --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift @@ -0,0 +1,52 @@ +// +// MovieTypeSectionView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct MovieTypeSectionView: View { + let cardCount: Int + let headerText: String + + var body: some View { + VStack(spacing: 10) { + HeaderView(headerText: headerText) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 15) { + ForEach(0.. Date: Tue, 14 Oct 2025 20:39:32 +0900 Subject: [PATCH 02/11] =?UTF-8?q?[feat]=20=EA=B2=80=EC=83=89=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Search/Components/EmptySearchView.swift | 34 +++++++++++++ .../Feature/Search/Components/SearchBar.swift | 48 +++++++++++++++++++ .../Search/Components/SearchView.swift | 36 ++++++++++++++ .../Feature/Search/MovieSearchView.swift | 25 ++++++++++ 4 files changed, 143 insertions(+) create mode 100644 MovieBooking/Feature/Search/Components/EmptySearchView.swift create mode 100644 MovieBooking/Feature/Search/Components/SearchBar.swift create mode 100644 MovieBooking/Feature/Search/Components/SearchView.swift create mode 100644 MovieBooking/Feature/Search/MovieSearchView.swift 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..e062718 --- /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 { + @Previewable @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..4ee8df2 --- /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: [String] = ["1","2","3","4"] + 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, id: \.self) { _ in + MovieCardView() + } + } + } + } + } +} + +#Preview { + SearchView() +} diff --git a/MovieBooking/Feature/Search/MovieSearchView.swift b/MovieBooking/Feature/Search/MovieSearchView.swift new file mode 100644 index 0000000..916b496 --- /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() + } + .padding(.horizontal, 20) + } +} + +#Preview { + MovieSearchView() +} From ec3e9e4f783f618221feb8cb091606023593c681 Mon Sep 17 00:00:00 2001 From: minneee Date: Wed, 15 Oct 2025 17:46:05 +0900 Subject: [PATCH 03/11] =?UTF-8?q?[feat]=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MovieBooking/Data/DTO/MovieDTO.swift | 7 ---- .../DTO/Response/MovieListResponseDTO.swift | 30 ++++++++++++++ .../Data/Repository/MockMovieRepository.swift | 15 +++++++ .../Data/Repository/MovieRepository.swift | 7 ++++ MovieBooking/Domain/Entity/Movie.swift | 35 ++++++++++++++++ .../Repository/MovieRepositoryProtocol.swift | 19 +++++++++ .../MovieList/Components/MovieCardView.swift | 9 +++-- .../Components/MovieTypeSectionView.swift | 11 ++--- .../Feature/MovieList/MovieListFeature.swift | 40 +++++++++++++++++++ .../Feature/MovieList/MovieListView.swift | 13 ++++-- .../Search/Components/SearchView.swift | 12 +++--- .../Feature/Search/MovieSearchView.swift | 2 +- 12 files changed, 174 insertions(+), 26 deletions(-) delete mode 100644 MovieBooking/Data/DTO/MovieDTO.swift create mode 100644 MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift create mode 100644 MovieBooking/Data/Repository/MockMovieRepository.swift create mode 100644 MovieBooking/Feature/MovieList/MovieListFeature.swift diff --git a/MovieBooking/Data/DTO/MovieDTO.swift b/MovieBooking/Data/DTO/MovieDTO.swift deleted file mode 100644 index 4749dc4..0000000 --- a/MovieBooking/Data/DTO/MovieDTO.swift +++ /dev/null @@ -1,7 +0,0 @@ -// -// MovieDTO.swift -// MovieBooking -// -// Created by 김민희 on 10/13/25. -// - diff --git a/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift b/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift new file mode 100644 index 0000000..0c67692 --- /dev/null +++ b/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift @@ -0,0 +1,30 @@ +// +// 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 +} diff --git a/MovieBooking/Data/Repository/MockMovieRepository.swift b/MovieBooking/Data/Repository/MockMovieRepository.swift new file mode 100644 index 0000000..68a4c92 --- /dev/null +++ b/MovieBooking/Data/Repository/MockMovieRepository.swift @@ -0,0 +1,15 @@ +// +// MockMovieRepository.swift +// MovieBooking +// +// Created by 김민희 on 10/15/25. +// + +import Foundation + +struct MockMovieRepository: MovieRepositoryProtocol { + func fetchMovies() 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..57c109a 100644 --- a/MovieBooking/Data/Repository/MovieRepository.swift +++ b/MovieBooking/Data/Repository/MovieRepository.swift @@ -5,3 +5,10 @@ // Created by 김민희 on 10/13/25. // +import Foundation + +struct MovieRepository: MovieRepositoryProtocol { + func fetchMovies() async throws -> [Movie] { + return [] + } +} diff --git a/MovieBooking/Domain/Entity/Movie.swift b/MovieBooking/Domain/Entity/Movie.swift index c86367c..0c1c9d2 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: String + public let title: String + public let overview: String + public let posterPath: String? + public let releaseDate: String + public let voteAverage: Double + + public init( + id: String, + 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..298237d 100644 --- a/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift +++ b/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift @@ -5,3 +5,22 @@ // Created by 김민희 on 10/13/25. // +import Foundation +import Dependencies + +protocol MovieRepositoryProtocol { + func fetchMovies() 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/MovieCardView.swift b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift index d4cb544..893c558 100644 --- a/MovieBooking/Feature/MovieList/Components/MovieCardView.swift +++ b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift @@ -8,6 +8,9 @@ import SwiftUI struct MovieCardView: View { + let movieTitle: String + let movieRating: Int + var body: some View { VStack(alignment: .leading, spacing: 10) { Image("movie") @@ -18,16 +21,16 @@ struct MovieCardView: View { .clipShape(RoundedRectangle(cornerRadius: 10)) .shadow(radius: 3, y: 3) - Text("Movie Title") + Text(movieTitle) .font(.system(size: 16)) .frame(width: 150, alignment: .leading) .lineLimit(1) - StarRatingView(rating: 3) + StarRatingView(rating: movieRating) } } } #Preview { - MovieCardView() + MovieCardView(movieTitle: "mvTitle", movieRating: 4) } diff --git a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift index 6cc7e98..141c022 100644 --- a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift +++ b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift @@ -10,14 +10,15 @@ import SwiftUI struct MovieTypeSectionView: View { let cardCount: Int let headerText: String + let movies: [Movie] var body: some View { VStack(spacing: 10) { HeaderView(headerText: headerText) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 15) { - ForEach(0..) +//} diff --git a/MovieBooking/Feature/MovieList/MovieListFeature.swift b/MovieBooking/Feature/MovieList/MovieListFeature.swift new file mode 100644 index 0000000..f8068b9 --- /dev/null +++ b/MovieBooking/Feature/MovieList/MovieListFeature.swift @@ -0,0 +1,40 @@ +// +// MovieListFeature.swift +// MovieBooking +// +// Created by 김민희 on 10/15/25. +// + +import Foundation +import ComposableArchitecture + +@Reducer +struct MovieListFeature { + @ObservableState + public struct State { + var movies: [Movie] = [] + var isLoading = false + } + + enum Action { + case onAppear + case fetchMovie + case selectMovie + } + + var body: some Reducer { + Reduce { state, action in + switch action { + case .onAppear: + return .none + + case .fetchMovie: + return .none + + case .selectMovie: + //TODO: 상세로 넘어감 + return .none + } + } + } +} diff --git a/MovieBooking/Feature/MovieList/MovieListView.swift b/MovieBooking/Feature/MovieList/MovieListView.swift index 5c34925..7ab5d89 100644 --- a/MovieBooking/Feature/MovieList/MovieListView.swift +++ b/MovieBooking/Feature/MovieList/MovieListView.swift @@ -6,19 +6,24 @@ // import SwiftUI +import ComposableArchitecture struct MovieListView: View { + let store: StoreOf + var body: some View { ScrollView(showsIndicators: false) { VStack(spacing: 20) { - MovieTypeSectionView(cardCount: 5, headerText: "Now Showing") - MovieTypeSectionView(cardCount: 4, headerText: "Coming Soon") - MovieTypeSectionView(cardCount: 3, headerText: "전체 영화") + MovieTypeSectionView(cardCount: 5, headerText: "Now Showing", movies: store.movies) + MovieTypeSectionView(cardCount: 4, headerText: "Coming Soon", movies: store.movies) + MovieTypeSectionView(cardCount: 3, headerText: "전체 영화", movies: store.movies) } } } } #Preview { - MovieListView() + MovieListView(store: Store(initialState: MovieListFeature.State(movies: Movie.mockData)) { + MovieListFeature() + }) } diff --git a/MovieBooking/Feature/Search/Components/SearchView.swift b/MovieBooking/Feature/Search/Components/SearchView.swift index 4ee8df2..f914118 100644 --- a/MovieBooking/Feature/Search/Components/SearchView.swift +++ b/MovieBooking/Feature/Search/Components/SearchView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SearchView: View { - let movies: [String] = ["1","2","3","4"] + let movies: [Movie] let columns: [GridItem] = [ GridItem(.flexible()), GridItem(.flexible()) @@ -22,8 +22,8 @@ struct SearchView: View { ScrollView { LazyVGrid(columns: columns, spacing: 20) { - ForEach(movies, id: \.self) { _ in - MovieCardView() + ForEach(movies) { movie in + MovieCardView(movieTitle: movie.title, movieRating: Int(movie.voteAverage)) } } } @@ -31,6 +31,6 @@ struct SearchView: View { } } -#Preview { - SearchView() -} +//#Preview { +// SearchView() +//} diff --git a/MovieBooking/Feature/Search/MovieSearchView.swift b/MovieBooking/Feature/Search/MovieSearchView.swift index 916b496..2531548 100644 --- a/MovieBooking/Feature/Search/MovieSearchView.swift +++ b/MovieBooking/Feature/Search/MovieSearchView.swift @@ -14,7 +14,7 @@ struct MovieSearchView: View { VStack(spacing: 20) { SearchBar(text: $searchText) - SearchView() + SearchView(movies: Movie.mockData) } .padding(.horizontal, 20) } From 7665847e831a4e952236008f2afe0a37650699d9 Mon Sep 17 00:00:00 2001 From: minneee Date: Tue, 14 Oct 2025 15:22:02 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[feat]=20=EC=98=81=ED=99=94=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/CircularArrowButton.swift | 41 +++++++++++++++ .../MovieList/Components/MovieCardView.swift | 33 ++++++++++++ .../Components/MovieTypeSectionView.swift | 52 +++++++++++++++++++ .../MovieList/Components/StarRatingView.swift | 24 +++++++++ .../Feature/MovieList/MovieListView.swift | 24 +++++++++ 5 files changed, 174 insertions(+) create mode 100644 MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift create mode 100644 MovieBooking/Feature/MovieList/Components/MovieCardView.swift create mode 100644 MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift create mode 100644 MovieBooking/Feature/MovieList/Components/StarRatingView.swift create mode 100644 MovieBooking/Feature/MovieList/MovieListView.swift diff --git a/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift b/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift new file mode 100644 index 0000000..00286fa --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/CircularArrowButton.swift @@ -0,0 +1,41 @@ +// +// CircularArrowButton.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +enum ArrowDirection { + case left + case right +} + +struct CircularArrowButton: View { + let direction: ArrowDirection + let action: () -> Void + + 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..d4cb544 --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift @@ -0,0 +1,33 @@ +// +// MovieCardView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct MovieCardView: View { + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Image("movie") + .resizable() + .scaledToFit() + .frame(width: 150, height: 200) + .background(.cyan) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(radius: 3, y: 3) + + Text("Movie Title") + .font(.system(size: 16)) + .frame(width: 150, alignment: .leading) + .lineLimit(1) + + StarRatingView(rating: 3) + } + } +} + +#Preview { + MovieCardView() +} diff --git a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift new file mode 100644 index 0000000..6cc7e98 --- /dev/null +++ b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift @@ -0,0 +1,52 @@ +// +// MovieTypeSectionView.swift +// MovieBooking +// +// Created by 김민희 on 10/14/25. +// + +import SwiftUI + +struct MovieTypeSectionView: View { + let cardCount: Int + let headerText: String + + var body: some View { + VStack(spacing: 10) { + HeaderView(headerText: headerText) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 15) { + ForEach(0.. Date: Tue, 14 Oct 2025 20:39:32 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[feat]=20=EA=B2=80=EC=83=89=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Search/Components/EmptySearchView.swift | 34 +++++++++++++ .../Feature/Search/Components/SearchBar.swift | 48 +++++++++++++++++++ .../Search/Components/SearchView.swift | 36 ++++++++++++++ .../Feature/Search/MovieSearchView.swift | 25 ++++++++++ 4 files changed, 143 insertions(+) create mode 100644 MovieBooking/Feature/Search/Components/EmptySearchView.swift create mode 100644 MovieBooking/Feature/Search/Components/SearchBar.swift create mode 100644 MovieBooking/Feature/Search/Components/SearchView.swift create mode 100644 MovieBooking/Feature/Search/MovieSearchView.swift 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..e062718 --- /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 { + @Previewable @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..4ee8df2 --- /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: [String] = ["1","2","3","4"] + 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, id: \.self) { _ in + MovieCardView() + } + } + } + } + } +} + +#Preview { + SearchView() +} diff --git a/MovieBooking/Feature/Search/MovieSearchView.swift b/MovieBooking/Feature/Search/MovieSearchView.swift new file mode 100644 index 0000000..916b496 --- /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() + } + .padding(.horizontal, 20) + } +} + +#Preview { + MovieSearchView() +} From 1cf34f7a0d35e06a687290d6018e5893d0664284 Mon Sep 17 00:00:00 2001 From: minneee Date: Wed, 15 Oct 2025 17:46:05 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[feat]=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xcshareddata/swiftpm/Package.resolved | 14 +++---- MovieBooking/Data/DTO/Response/MovieDTO.swift | 10 ----- .../DTO/Response/MovieListResponseDTO.swift | 30 ++++++++++++++ .../Data/Repository/MockMovieRepository.swift | 15 +++++++ .../Data/Repository/MovieRepository.swift | 7 ++++ MovieBooking/Domain/Entity/Movie.swift | 35 ++++++++++++++++ .../Repository/MovieRepositoryProtocol.swift | 19 +++++++++ .../MovieList/Components/MovieCardView.swift | 9 +++-- .../Components/MovieTypeSectionView.swift | 11 ++--- .../Feature/MovieList/MovieListFeature.swift | 40 +++++++++++++++++++ .../Feature/MovieList/MovieListView.swift | 13 ++++-- .../Search/Components/SearchView.swift | 12 +++--- .../Feature/Search/MovieSearchView.swift | 2 +- 13 files changed, 181 insertions(+), 36 deletions(-) delete mode 100644 MovieBooking/Data/DTO/Response/MovieDTO.swift create mode 100644 MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift create mode 100644 MovieBooking/Data/Repository/MockMovieRepository.swift create mode 100644 MovieBooking/Feature/MovieList/MovieListFeature.swift 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..0c67692 --- /dev/null +++ b/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift @@ -0,0 +1,30 @@ +// +// 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 +} diff --git a/MovieBooking/Data/Repository/MockMovieRepository.swift b/MovieBooking/Data/Repository/MockMovieRepository.swift new file mode 100644 index 0000000..68a4c92 --- /dev/null +++ b/MovieBooking/Data/Repository/MockMovieRepository.swift @@ -0,0 +1,15 @@ +// +// MockMovieRepository.swift +// MovieBooking +// +// Created by 김민희 on 10/15/25. +// + +import Foundation + +struct MockMovieRepository: MovieRepositoryProtocol { + func fetchMovies() 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..57c109a 100644 --- a/MovieBooking/Data/Repository/MovieRepository.swift +++ b/MovieBooking/Data/Repository/MovieRepository.swift @@ -5,3 +5,10 @@ // Created by 김민희 on 10/13/25. // +import Foundation + +struct MovieRepository: MovieRepositoryProtocol { + func fetchMovies() async throws -> [Movie] { + return [] + } +} diff --git a/MovieBooking/Domain/Entity/Movie.swift b/MovieBooking/Domain/Entity/Movie.swift index c86367c..0c1c9d2 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: String + public let title: String + public let overview: String + public let posterPath: String? + public let releaseDate: String + public let voteAverage: Double + + public init( + id: String, + 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..298237d 100644 --- a/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift +++ b/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift @@ -5,3 +5,22 @@ // Created by 김민희 on 10/13/25. // +import Foundation +import Dependencies + +protocol MovieRepositoryProtocol { + func fetchMovies() 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/MovieCardView.swift b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift index d4cb544..893c558 100644 --- a/MovieBooking/Feature/MovieList/Components/MovieCardView.swift +++ b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift @@ -8,6 +8,9 @@ import SwiftUI struct MovieCardView: View { + let movieTitle: String + let movieRating: Int + var body: some View { VStack(alignment: .leading, spacing: 10) { Image("movie") @@ -18,16 +21,16 @@ struct MovieCardView: View { .clipShape(RoundedRectangle(cornerRadius: 10)) .shadow(radius: 3, y: 3) - Text("Movie Title") + Text(movieTitle) .font(.system(size: 16)) .frame(width: 150, alignment: .leading) .lineLimit(1) - StarRatingView(rating: 3) + StarRatingView(rating: movieRating) } } } #Preview { - MovieCardView() + MovieCardView(movieTitle: "mvTitle", movieRating: 4) } diff --git a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift index 6cc7e98..141c022 100644 --- a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift +++ b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift @@ -10,14 +10,15 @@ import SwiftUI struct MovieTypeSectionView: View { let cardCount: Int let headerText: String + let movies: [Movie] var body: some View { VStack(spacing: 10) { HeaderView(headerText: headerText) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 15) { - ForEach(0..) +//} diff --git a/MovieBooking/Feature/MovieList/MovieListFeature.swift b/MovieBooking/Feature/MovieList/MovieListFeature.swift new file mode 100644 index 0000000..f8068b9 --- /dev/null +++ b/MovieBooking/Feature/MovieList/MovieListFeature.swift @@ -0,0 +1,40 @@ +// +// MovieListFeature.swift +// MovieBooking +// +// Created by 김민희 on 10/15/25. +// + +import Foundation +import ComposableArchitecture + +@Reducer +struct MovieListFeature { + @ObservableState + public struct State { + var movies: [Movie] = [] + var isLoading = false + } + + enum Action { + case onAppear + case fetchMovie + case selectMovie + } + + var body: some Reducer { + Reduce { state, action in + switch action { + case .onAppear: + return .none + + case .fetchMovie: + return .none + + case .selectMovie: + //TODO: 상세로 넘어감 + return .none + } + } + } +} diff --git a/MovieBooking/Feature/MovieList/MovieListView.swift b/MovieBooking/Feature/MovieList/MovieListView.swift index 5c34925..7ab5d89 100644 --- a/MovieBooking/Feature/MovieList/MovieListView.swift +++ b/MovieBooking/Feature/MovieList/MovieListView.swift @@ -6,19 +6,24 @@ // import SwiftUI +import ComposableArchitecture struct MovieListView: View { + let store: StoreOf + var body: some View { ScrollView(showsIndicators: false) { VStack(spacing: 20) { - MovieTypeSectionView(cardCount: 5, headerText: "Now Showing") - MovieTypeSectionView(cardCount: 4, headerText: "Coming Soon") - MovieTypeSectionView(cardCount: 3, headerText: "전체 영화") + MovieTypeSectionView(cardCount: 5, headerText: "Now Showing", movies: store.movies) + MovieTypeSectionView(cardCount: 4, headerText: "Coming Soon", movies: store.movies) + MovieTypeSectionView(cardCount: 3, headerText: "전체 영화", movies: store.movies) } } } } #Preview { - MovieListView() + MovieListView(store: Store(initialState: MovieListFeature.State(movies: Movie.mockData)) { + MovieListFeature() + }) } diff --git a/MovieBooking/Feature/Search/Components/SearchView.swift b/MovieBooking/Feature/Search/Components/SearchView.swift index 4ee8df2..f914118 100644 --- a/MovieBooking/Feature/Search/Components/SearchView.swift +++ b/MovieBooking/Feature/Search/Components/SearchView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SearchView: View { - let movies: [String] = ["1","2","3","4"] + let movies: [Movie] let columns: [GridItem] = [ GridItem(.flexible()), GridItem(.flexible()) @@ -22,8 +22,8 @@ struct SearchView: View { ScrollView { LazyVGrid(columns: columns, spacing: 20) { - ForEach(movies, id: \.self) { _ in - MovieCardView() + ForEach(movies) { movie in + MovieCardView(movieTitle: movie.title, movieRating: Int(movie.voteAverage)) } } } @@ -31,6 +31,6 @@ struct SearchView: View { } } -#Preview { - SearchView() -} +//#Preview { +// SearchView() +//} diff --git a/MovieBooking/Feature/Search/MovieSearchView.swift b/MovieBooking/Feature/Search/MovieSearchView.swift index 916b496..2531548 100644 --- a/MovieBooking/Feature/Search/MovieSearchView.swift +++ b/MovieBooking/Feature/Search/MovieSearchView.swift @@ -14,7 +14,7 @@ struct MovieSearchView: View { VStack(spacing: 20) { SearchBar(text: $searchText) - SearchView() + SearchView(movies: Movie.mockData) } .padding(.horizontal, 20) } From c989ba525e56c5c523659b45cb55e8e0f955d79e Mon Sep 17 00:00:00 2001 From: minneee Date: Wed, 15 Oct 2025 19:59:12 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[feat]=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Response/MovieListResponseDTO.swift | 13 ++++ .../Data/DataSources/MovieDataSource.swift | 23 ++++++ .../Data/Repository/MockMovieRepository.swift | 10 ++- .../Data/Repository/MovieRepository.swift | 23 +++++- MovieBooking/Domain/Entity/Movie.swift | 10 +-- .../Repository/MovieRepositoryProtocol.swift | 4 +- .../MovieList/Components/MovieCardView.swift | 47 ++++++++++--- .../Components/MovieTypeSectionView.swift | 2 +- .../Feature/MovieList/MovieListFeature.swift | 70 ++++++++++++++++++- .../Feature/MovieList/MovieListView.swift | 19 ++--- .../Feature/Search/Components/SearchBar.swift | 2 +- .../Search/Components/SearchView.swift | 2 +- 12 files changed, 195 insertions(+), 30 deletions(-) diff --git a/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift b/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift index 0c67692..38ea35d 100644 --- a/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift +++ b/MovieBooking/Data/DTO/Response/MovieListResponseDTO.swift @@ -28,3 +28,16 @@ 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..3012d4a 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,25 @@ struct DefaultMovieDataSource: MovieDataSource { ) async throws -> MovieDetailResponseDTO { try await provider.request(MovieTarget.movieDetail(id: id)) } + + func movieList( + category: MovieCategory, + page: Int = 1 + ) async throws -> MovieListResponseDTO { + try await provider.request(MovieTarget.movieList(category: category, page: page)) + } } enum MovieTarget { case movieDetail(id: String) + case movieList(category: MovieCategory, page: Int = 1) +} + +enum MovieCategory: String { + case popular + case nowPlaying = "now_playing" + case topRated = "top_rated" + case upcoming } extension MovieTarget: TargetType { @@ -38,6 +54,8 @@ extension MovieTarget: TargetType { switch self { case .movieDetail(let id): return "/\(id)" + case .movieList(let category, _): + return "/\(category.rawValue)" } } @@ -45,6 +63,8 @@ extension MovieTarget: TargetType { switch self { case .movieDetail: return .get + case .movieList: + return .get } } @@ -52,6 +72,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 index 68a4c92..d544806 100644 --- a/MovieBooking/Data/Repository/MockMovieRepository.swift +++ b/MovieBooking/Data/Repository/MockMovieRepository.swift @@ -8,7 +8,15 @@ import Foundation struct MockMovieRepository: MovieRepositoryProtocol { - func fetchMovies() async throws -> [Movie] { + 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 57c109a..09d6dea 100644 --- a/MovieBooking/Data/Repository/MovieRepository.swift +++ b/MovieBooking/Data/Repository/MovieRepository.swift @@ -8,7 +8,26 @@ import Foundation struct MovieRepository: MovieRepositoryProtocol { - func fetchMovies() async throws -> [Movie] { - return [] + 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/Domain/Entity/Movie.swift b/MovieBooking/Domain/Entity/Movie.swift index 0c1c9d2..2045266 100644 --- a/MovieBooking/Domain/Entity/Movie.swift +++ b/MovieBooking/Domain/Entity/Movie.swift @@ -8,7 +8,7 @@ import Foundation public struct Movie: Identifiable, Equatable { - public let id: String + public let id: Int public let title: String public let overview: String public let posterPath: String? @@ -16,7 +16,7 @@ public struct Movie: Identifiable, Equatable { public let voteAverage: Double public init( - id: String, + id: Int, title: String, overview: String, posterPath: String?, @@ -34,9 +34,9 @@ public struct Movie: Identifiable, Equatable { 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 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 298237d..04050c4 100644 --- a/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift +++ b/MovieBooking/Domain/Repository/MovieRepositoryProtocol.swift @@ -9,7 +9,9 @@ import Foundation import Dependencies protocol MovieRepositoryProtocol { - func fetchMovies() async throws -> [Movie] + func fetchNowPlayingMovies() async throws -> [Movie] + func fetchUpcomingMovies() async throws -> [Movie] + func fetchPopularMovies() async throws -> [Movie] } private enum MovieRepositoryKey: DependencyKey { diff --git a/MovieBooking/Feature/MovieList/Components/MovieCardView.swift b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift index 893c558..383eca6 100644 --- a/MovieBooking/Feature/MovieList/Components/MovieCardView.swift +++ b/MovieBooking/Feature/MovieList/Components/MovieCardView.swift @@ -10,16 +10,47 @@ import SwiftUI struct MovieCardView: View { let movieTitle: String let movieRating: Int + let posterPath: String? + + 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) { - Image("movie") - .resizable() - .scaledToFit() - .frame(width: 150, height: 200) - .background(.cyan) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .shadow(radius: 3, y: 3) + 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)) @@ -32,5 +63,5 @@ struct MovieCardView: View { } #Preview { - MovieCardView(movieTitle: "mvTitle", movieRating: 4) + MovieCardView(movieTitle: "mvTitle", movieRating: 4, posterPath: nil) } diff --git a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift index 141c022..b57092e 100644 --- a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift +++ b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift @@ -18,7 +18,7 @@ struct MovieTypeSectionView: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 15) { ForEach(movies) { movie in - MovieCardView(movieTitle: movie.title, movieRating: Int(movie.voteAverage)) + MovieCardView(movieTitle: movie.title, movieRating: Int(movie.voteAverage / 2), posterPath: movie.posterPath) } } } diff --git a/MovieBooking/Feature/MovieList/MovieListFeature.swift b/MovieBooking/Feature/MovieList/MovieListFeature.swift index f8068b9..4b66c13 100644 --- a/MovieBooking/Feature/MovieList/MovieListFeature.swift +++ b/MovieBooking/Feature/MovieList/MovieListFeature.swift @@ -10,15 +10,22 @@ import ComposableArchitecture @Reducer struct MovieListFeature { + @Dependency(\.movieRepository) var movieRepository + @ObservableState public struct State { - var movies: [Movie] = [] + var nowPlayingMovies: [Movie] = [] + var upcomingMovies: [Movie] = [] + var popularMovies: [Movie] = [] var isLoading = false } enum Action { case onAppear case fetchMovie + case fetchNowPlayingResponse(Result<[Movie], Error>) + case fetchUpcomingResponse(Result<[Movie], Error>) + case fetchPopularResponse(Result<[Movie], Error>) case selectMovie } @@ -26,9 +33,68 @@ struct MovieListFeature { Reduce { state, action in switch action { case .onAppear: - return .none + return .send(.fetchMovie) case .fetchMovie: + state.isLoading = true + 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)): + print("❌ 영화 가져오기 실패: \(error)") + state.isLoading = false return .none case .selectMovie: diff --git a/MovieBooking/Feature/MovieList/MovieListView.swift b/MovieBooking/Feature/MovieList/MovieListView.swift index 7ab5d89..9e0a24b 100644 --- a/MovieBooking/Feature/MovieList/MovieListView.swift +++ b/MovieBooking/Feature/MovieList/MovieListView.swift @@ -14,16 +14,19 @@ struct MovieListView: View { var body: some View { ScrollView(showsIndicators: false) { VStack(spacing: 20) { - MovieTypeSectionView(cardCount: 5, headerText: "Now Showing", movies: store.movies) - MovieTypeSectionView(cardCount: 4, headerText: "Coming Soon", movies: store.movies) - MovieTypeSectionView(cardCount: 3, headerText: "전체 영화", movies: store.movies) + 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() + } } } -#Preview { - MovieListView(store: Store(initialState: MovieListFeature.State(movies: Movie.mockData)) { - MovieListFeature() - }) -} +//#Preview { +// MovieListView(store: Store(initialState: MovieListFeature.State(movies: Movie.mockData)) { +// MovieListFeature() +// }) +//} diff --git a/MovieBooking/Feature/Search/Components/SearchBar.swift b/MovieBooking/Feature/Search/Components/SearchBar.swift index e062718..61d1ee8 100644 --- a/MovieBooking/Feature/Search/Components/SearchBar.swift +++ b/MovieBooking/Feature/Search/Components/SearchBar.swift @@ -43,6 +43,6 @@ struct SearchBar: View { } #Preview { - @Previewable @State var text = "" + @State var text = "" SearchBar(text: $text) } diff --git a/MovieBooking/Feature/Search/Components/SearchView.swift b/MovieBooking/Feature/Search/Components/SearchView.swift index f914118..049caf6 100644 --- a/MovieBooking/Feature/Search/Components/SearchView.swift +++ b/MovieBooking/Feature/Search/Components/SearchView.swift @@ -23,7 +23,7 @@ struct SearchView: View { ScrollView { LazyVGrid(columns: columns, spacing: 20) { ForEach(movies) { movie in - MovieCardView(movieTitle: movie.title, movieRating: Int(movie.voteAverage)) + MovieCardView(movieTitle: movie.title, movieRating: Int(movie.voteAverage / 2), posterPath: movie.posterPath) } } } From 21a5d526c62247d2ce72045c2b107e3758fd00f6 Mon Sep 17 00:00:00 2001 From: minneee Date: Thu, 16 Oct 2025 16:53:55 +0900 Subject: [PATCH 08/11] =?UTF-8?q?[feat]=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/MovieList/MovieListFeature.swift | 30 ++++++++++- .../Feature/MovieList/MovieListView.swift | 3 +- .../Feature/Splash/View/SplashView.swift | 52 +++++++++++-------- 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/MovieBooking/Feature/MovieList/MovieListFeature.swift b/MovieBooking/Feature/MovieList/MovieListFeature.swift index 4b66c13..760e986 100644 --- a/MovieBooking/Feature/MovieList/MovieListFeature.swift +++ b/MovieBooking/Feature/MovieList/MovieListFeature.swift @@ -7,6 +7,7 @@ import Foundation import ComposableArchitecture +import SwiftUI @Reducer struct MovieListFeature { @@ -18,6 +19,7 @@ struct MovieListFeature { var upcomingMovies: [Movie] = [] var popularMovies: [Movie] = [] var isLoading = false + @Presents var alert: AlertState? } enum Action { @@ -27,6 +29,11 @@ struct MovieListFeature { case fetchUpcomingResponse(Result<[Movie], Error>) case fetchPopularResponse(Result<[Movie], Error>) case selectMovie + case alert(PresentationAction) + + enum Alert: Equatable { + case retry + } } var body: some Reducer { @@ -37,6 +44,7 @@ struct MovieListFeature { case .fetchMovie: state.isLoading = true + state.alert = nil return .run { send in await withThrowingTaskGroup(of: Void.self) { group in // Now Playing @@ -93,13 +101,33 @@ struct MovieListFeature { case let .fetchNowPlayingResponse(.failure(error)), let .fetchUpcomingResponse(.failure(error)), let .fetchPopularResponse(.failure(error)): - print("❌ 영화 가져오기 실패: \(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 index 9e0a24b..cc2e110 100644 --- a/MovieBooking/Feature/MovieList/MovieListView.swift +++ b/MovieBooking/Feature/MovieList/MovieListView.swift @@ -9,7 +9,7 @@ import SwiftUI import ComposableArchitecture struct MovieListView: View { - let store: StoreOf + @Perception.Bindable var store: StoreOf var body: some View { ScrollView(showsIndicators: false) { @@ -22,6 +22,7 @@ struct MovieListView: View { .task { await store.send(.onAppear).finish() } + .alert($store.scope(state: \.alert, action: \.alert)) } } diff --git a/MovieBooking/Feature/Splash/View/SplashView.swift b/MovieBooking/Feature/Splash/View/SplashView.swift index 82a2f72..c02b569 100644 --- a/MovieBooking/Feature/Splash/View/SplashView.swift +++ b/MovieBooking/Feature/Splash/View/SplashView.swift @@ -13,31 +13,37 @@ struct SplashView: View { @State var store: StoreOf var body: some View { - ZStack { - LinearGradient( - gradient: Gradient(colors: [ - .white, - .white, - .basicPurple.opacity(0.05) - ]), - startPoint: .top, - endPoint: .bottom - ) - .ignoresSafeArea() - - VStack(spacing: 24) { - - splashLogo() +// ZStack { +// LinearGradient( +// gradient: Gradient(colors: [ +// .white, +// .white, +// .basicPurple.opacity(0.05) +// ]), +// startPoint: .top, +// endPoint: .bottom +// ) +// .ignoresSafeArea() +// +// VStack(spacing: 24) { +// +// splashLogo() +// +// titleView() +// } +// .scaleEffect(store.fadeOut ? 0.95 : 1.0) +// .opacity(store.fadeOut ? 0.0 : 1.0) +// .animation(.easeInOut(duration: 1), value: store.fadeOut) +// .onAppear { +// send(.onAppear) +// } +// } - titleView() + MovieListView( + store: Store(initialState: MovieListFeature.State()) { + MovieListFeature() } - .scaleEffect(store.fadeOut ? 0.95 : 1.0) - .opacity(store.fadeOut ? 0.0 : 1.0) - .animation(.easeInOut(duration: 1), value: store.fadeOut) - .onAppear { - send(.onAppear) - } - } + ) } } From 96122ca15115b4ddcc54fb66380eb3f4d644ed51 Mon Sep 17 00:00:00 2001 From: minneee Date: Thu, 16 Oct 2025 17:01:39 +0900 Subject: [PATCH 09/11] =?UTF-8?q?[feat]=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=8F=99=EC=9E=91=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/MovieTypeSectionView.swift | 45 ++++++++++++---- .../Feature/Splash/View/SplashView.swift | 52 ++++++++----------- 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift index b57092e..cdc90d1 100644 --- a/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift +++ b/MovieBooking/Feature/MovieList/Components/MovieTypeSectionView.swift @@ -11,14 +11,39 @@ struct MovieTypeSectionView: View { let cardCount: Int let headerText: String let movies: [Movie] + @State private var currentIndex: Int = 0 var body: some View { VStack(spacing: 10) { - HeaderView(headerText: headerText) - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 15) { - ForEach(movies) { movie in - MovieCardView(movieTitle: movie.title, movieRating: Int(movie.voteAverage / 2), posterPath: movie.posterPath) + 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) } } } @@ -29,6 +54,8 @@ struct MovieTypeSectionView: View { struct HeaderView: View { let headerText: String + let onLeftTapped: () -> Void + let onRightTapped: () -> Void var body: some View { HStack(spacing: 10) { @@ -37,13 +64,9 @@ struct HeaderView: View { Spacer() - CircularArrowButton(direction: .left) { - print("left") - } + CircularArrowButton(direction: .left, action: onLeftTapped) - CircularArrowButton(direction: .right) { - print("right") - } + CircularArrowButton(direction: .right, action: onRightTapped) } } } diff --git a/MovieBooking/Feature/Splash/View/SplashView.swift b/MovieBooking/Feature/Splash/View/SplashView.swift index c02b569..82a2f72 100644 --- a/MovieBooking/Feature/Splash/View/SplashView.swift +++ b/MovieBooking/Feature/Splash/View/SplashView.swift @@ -13,37 +13,31 @@ struct SplashView: View { @State var store: StoreOf var body: some View { -// ZStack { -// LinearGradient( -// gradient: Gradient(colors: [ -// .white, -// .white, -// .basicPurple.opacity(0.05) -// ]), -// startPoint: .top, -// endPoint: .bottom -// ) -// .ignoresSafeArea() -// -// VStack(spacing: 24) { -// -// splashLogo() -// -// titleView() -// } -// .scaleEffect(store.fadeOut ? 0.95 : 1.0) -// .opacity(store.fadeOut ? 0.0 : 1.0) -// .animation(.easeInOut(duration: 1), value: store.fadeOut) -// .onAppear { -// send(.onAppear) -// } -// } + ZStack { + LinearGradient( + gradient: Gradient(colors: [ + .white, + .white, + .basicPurple.opacity(0.05) + ]), + startPoint: .top, + endPoint: .bottom + ) + .ignoresSafeArea() - MovieListView( - store: Store(initialState: MovieListFeature.State()) { - MovieListFeature() + VStack(spacing: 24) { + + splashLogo() + + titleView() } - ) + .scaleEffect(store.fadeOut ? 0.95 : 1.0) + .opacity(store.fadeOut ? 0.0 : 1.0) + .animation(.easeInOut(duration: 1), value: store.fadeOut) + .onAppear { + send(.onAppear) + } + } } } From 3902e61cbcce67cd79aa3c3caba70d72e60ac742 Mon Sep 17 00:00:00 2001 From: minneee Date: Fri, 17 Oct 2025 11:41:12 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[feat]=20=EC=98=81=ED=99=94=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/Search/MovieSearchFeature.swift | 68 +++++++++++++++++++ .../Feature/Search/MovieSearchView.swift | 34 ++++++++-- 2 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 MovieBooking/Feature/Search/MovieSearchFeature.swift diff --git a/MovieBooking/Feature/Search/MovieSearchFeature.swift b/MovieBooking/Feature/Search/MovieSearchFeature.swift new file mode 100644 index 0000000..188cabf --- /dev/null +++ b/MovieBooking/Feature/Search/MovieSearchFeature.swift @@ -0,0 +1,68 @@ +// +// MovieSearchFeature.swift +// MovieBooking +// +// Created by 김민희 on 10/16/25. +// + +import ComposableArchitecture +import Foundation + +@Reducer +struct MovieSearchFeature { + @ObservableState + struct State: Equatable { + var nowPlayingMovies: [Movie] = [] + var upcomingMovies: [Movie] = [] + var popularMovies: [Movie] = [] + var searchText: String = "" + } + + enum Action: BindableAction { + case updateMovieLists(nowPlaying: [Movie], upcoming: [Movie], popular: [Movie]) + case binding(BindingAction) + } + + var body: some Reducer { + BindingReducer() + Reduce { state, action in + switch action { + case let .updateMovieLists(nowPlaying, upcoming, popular): + state.nowPlayingMovies = nowPlaying + state.upcomingMovies = upcoming + state.popularMovies = popular + return .none + + case .binding: + return .none + } + } + } +} + + +extension MovieSearchFeature.State { + var trimmedKeyword: String { + searchText.trimmingCharacters(in: .whitespacesAndNewlines) + } + + var aggregatedMovies: [Movie] { + let combined = nowPlayingMovies + upcomingMovies + popularMovies + var unique: [Movie] = [] + var seenIDs = Set() + + for movie in combined where seenIDs.insert(movie.id).inserted { + unique.append(movie) + } + + return unique + } + + var filteredMovies: [Movie] { + guard !trimmedKeyword.isEmpty else { return [] } + + return aggregatedMovies.filter { + $0.title.localizedCaseInsensitiveContains(trimmedKeyword) + } + } +} diff --git a/MovieBooking/Feature/Search/MovieSearchView.swift b/MovieBooking/Feature/Search/MovieSearchView.swift index 2531548..a04960e 100644 --- a/MovieBooking/Feature/Search/MovieSearchView.swift +++ b/MovieBooking/Feature/Search/MovieSearchView.swift @@ -6,20 +6,42 @@ // import SwiftUI +import ComposableArchitecture struct MovieSearchView: View { - @State private var searchText = "" + @Perception.Bindable var store: StoreOf var body: some View { - VStack(spacing: 20) { - SearchBar(text: $searchText) + WithPerceptionTracking { + VStack(spacing: 0) { + SearchBar(text: $store.searchText) + .padding(.bottom, 20) - SearchView(movies: Movie.mockData) + Group { + if store.trimmedKeyword.isEmpty { + EmptySearchView() + } else { + SearchView(movies: store.filteredMovies) + } + } + .frame(maxHeight: .infinity) + } + .padding(.top, 20) + .padding(.horizontal, 20) } - .padding(.horizontal, 20) } } #Preview { - MovieSearchView() + MovieSearchView( + store: Store( + initialState: MovieSearchFeature.State( + nowPlayingMovies: Movie.mockData, + upcomingMovies: Movie.mockData, + popularMovies: Movie.mockData + ) + ) { + MovieSearchFeature() + } + ) } From c2bea06f451294be8aaa1a95530e2d7a476d323a Mon Sep 17 00:00:00 2001 From: minneee Date: Fri, 17 Oct 2025 17:53:52 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[chore]=20=EC=98=81=ED=99=94=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=95=A9=EC=B9=98=EA=B8=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MovieBooking/Domain/Entity/Movie.swift | 2 +- MovieBooking/Feature/Search/MovieSearchFeature.swift | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/MovieBooking/Domain/Entity/Movie.swift b/MovieBooking/Domain/Entity/Movie.swift index 2045266..c21d273 100644 --- a/MovieBooking/Domain/Entity/Movie.swift +++ b/MovieBooking/Domain/Entity/Movie.swift @@ -7,7 +7,7 @@ import Foundation -public struct Movie: Identifiable, Equatable { +public struct Movie: Identifiable, Equatable, Hashable { public let id: Int public let title: String public let overview: String diff --git a/MovieBooking/Feature/Search/MovieSearchFeature.swift b/MovieBooking/Feature/Search/MovieSearchFeature.swift index 188cabf..d86d541 100644 --- a/MovieBooking/Feature/Search/MovieSearchFeature.swift +++ b/MovieBooking/Feature/Search/MovieSearchFeature.swift @@ -47,15 +47,7 @@ extension MovieSearchFeature.State { } var aggregatedMovies: [Movie] { - let combined = nowPlayingMovies + upcomingMovies + popularMovies - var unique: [Movie] = [] - var seenIDs = Set() - - for movie in combined where seenIDs.insert(movie.id).inserted { - unique.append(movie) - } - - return unique + Array(Set(nowPlayingMovies + upcomingMovies + popularMovies)) } var filteredMovies: [Movie] {