From a6adbb468bb53e8c67ddd02c52f4c434a6150b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=92=E1=85=A9=E1=86=BC=E1=84=89=E1=85=A5=E1=86=A8?= =?UTF-8?q?=E1=84=92=E1=85=A7=E1=86=AB?= Date: Fri, 17 Oct 2025 11:33:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat.=20=EC=98=81=ED=99=94=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=B9=B4=EB=93=9C=20=EB=B7=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Response/MovieDetailResponseDTO.swift | 10 +- MovieBooking/Domain/Entity/MovieDetail.swift | 51 +++++++ .../MovieDetail/Components/GenreLabel.swift | 30 ++++ .../Components/MovieDetailCardView.swift | 128 ++++++++++++++++++ .../Components/StarRatingView.swift | 86 ++++++++++++ .../Feature/MovieDetail/MovieDetailView.swift | 18 +++ 6 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 MovieBooking/Domain/Entity/MovieDetail.swift create mode 100644 MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift create mode 100644 MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift create mode 100644 MovieBooking/Feature/MovieDetail/Components/StarRatingView.swift create mode 100644 MovieBooking/Feature/MovieDetail/MovieDetailView.swift diff --git a/MovieBooking/Data/DTO/Response/MovieDetailResponseDTO.swift b/MovieBooking/Data/DTO/Response/MovieDetailResponseDTO.swift index 8776843..9db973f 100644 --- a/MovieBooking/Data/DTO/Response/MovieDetailResponseDTO.swift +++ b/MovieBooking/Data/DTO/Response/MovieDetailResponseDTO.swift @@ -22,11 +22,7 @@ struct MovieDetailResponseDTO: Decodable { let popularity: Double } -// MARK: - Nested Types - -extension MovieDetailResponseDTO { - struct Genre: Decodable { - let id: Int - let name: String - } +struct Genre: Decodable { + let id: Int + let name: String } diff --git a/MovieBooking/Domain/Entity/MovieDetail.swift b/MovieBooking/Domain/Entity/MovieDetail.swift new file mode 100644 index 0000000..f8f0d11 --- /dev/null +++ b/MovieBooking/Domain/Entity/MovieDetail.swift @@ -0,0 +1,51 @@ +// +// MovieDetail.swift +// MovieBooking +// +// Created by 홍석현 on 10/16/25. +// + +import Foundation + +struct MovieDetail { + let title: String + let genres: String + let releaseDate: Date? + let runningTime: Int + let rating: Double + let posterPath: String? + let summary: String + + init( + title: String, + genres: String, + releaseDate: String, + runningTime: Int, + rating: Double, + posterPath: String?, + summary: String + ) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd." + + self.title = title + self.genres = genres + self.releaseDate = dateFormatter.date(from: releaseDate) + self.runningTime = runningTime + self.rating = rating + self.posterPath = posterPath + self.summary = summary + } +} + +extension MovieDetail { + static let mockData: MovieDetail = MovieDetail( + title: "The Shawshank Redemption", + genres: "Drama", + releaseDate: "1994-9-23.", + runningTime: 142 * 60, + rating: 8.7, + posterPath: nil, + summary: "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency." + ) +} diff --git a/MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift b/MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift new file mode 100644 index 0000000..b64836e --- /dev/null +++ b/MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift @@ -0,0 +1,30 @@ +// +// GenreLabel.swift +// MovieBooking +// +// Created by 홍석현 on 10/17/25. +// + +import SwiftUI + +struct GenreLabel: View { + let genre: Genre + + var body: some View { + Text(genre.name) + .font(.caption) + .foregroundColor(.primary) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .background(Color.purple.opacity(0.2)) + .clipShape(Capsule()) + .overlay( + Capsule() + .stroke(Color.purple.opacity(0.5), lineWidth: 1) + ) + } +} + +#Preview { + GenreLabel(genre: Genre(id: 0, name: "Drama")) +} diff --git a/MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift b/MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift new file mode 100644 index 0000000..3bc19de --- /dev/null +++ b/MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift @@ -0,0 +1,128 @@ +// +// MovieDetailCardView.swift +// MovieBooking +// +// Created by 홍석현 on 10/16/25. +// + +import SwiftUI + +struct MovieDetailCardView: View { + private let model: MovieDetail + + init(model: MovieDetail) { + self.model = model + } + + var body: some View { + VStack(alignment: .leading, spacing: 24) { + GenreLabel(genre: Genre(id: 0, name: "Drama")) + + VStack(alignment: .leading, spacing: 12) { + titleView + + StarRatingView(rating: model.rating) + + HStack(spacing: 24) { + ReleaseDateView(date: model.releaseDate) + + RunningTimeView(model.runningTime) + } + } + + VStack(alignment: .leading, spacing: 12) { + Text("줄거리") + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(Color.primary) + Text(model.summary) + .font(.system(size: 16, weight: .regular)) + .lineSpacing(3) + .foregroundStyle(Color(hex: "757575")) + } + + Button { + print("예매하기 버튼 눌림") + } label: { + Text("예매하기") + .frame(maxWidth: .infinity) + .font(.system(size: 16, weight: .bold)) + .foregroundStyle(Color.white) + .padding(.vertical, 16) + .background(Color.basicPurple) + .clipShape(Capsule()) + .shadow(color: .black.opacity(0.2), radius: 5, y: 5) + } + + } + .padding(24) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .black.opacity(0.2), radius: 10, y: 10) + } +} + +extension MovieDetailCardView { + // 영화 제목 + var titleView: some View { + Text(model.title) + .font(.system(size: 36, weight: .light)) + .frame(maxWidth: 200, alignment: .leading) + .lineSpacing(0) + } + + // 상영 날짜 + struct ReleaseDateView: View { + private let date: Date? + + init(date: Date?) { + self.date = date + } + + private var displayText: String { + guard let date = date else { return "개봉일 미정" } + let formatter = DateFormatter() + formatter.dateFormat = "yyyy. MM. dd." + return formatter.string(from: date) + } + + var body: some View { + HStack { + Image(systemName: "calendar") + .foregroundStyle(Color.secondary) + + Text(displayText) + } + .font(.system(size: 16)) + .foregroundStyle(Color.secondary) + } + } + + // 러닝 타임 + struct RunningTimeView: View { + private let runningSecond: Int + + private var displayText: String { + return "\(runningSecond / 60)분" + } + + init(_ runningSecond: Int) { + self.runningSecond = runningSecond + } + + var body: some View { + HStack { + Image(systemName: "clock") + .foregroundStyle(Color.secondary) + + Text(displayText) + } + .font(.system(size: 16)) + .foregroundStyle(Color.secondary) + } + } +} + +#Preview { + MovieDetailCardView(model: .mockData) + .padding() +} diff --git a/MovieBooking/Feature/MovieDetail/Components/StarRatingView.swift b/MovieBooking/Feature/MovieDetail/Components/StarRatingView.swift new file mode 100644 index 0000000..7b59b48 --- /dev/null +++ b/MovieBooking/Feature/MovieDetail/Components/StarRatingView.swift @@ -0,0 +1,86 @@ +// +// StarRatingView.swift +// MovieBooking +// +// Created by 홍석현 on 10/17/25. +// + +import SwiftUI + +struct StarRatingView: View { + private let rating: Double // 0.0 ~ 10.0 범위 + private let maxStars: Int = 5 + + init(rating: Double) { + self.rating = rating + } + + private var normalizedRating: Double { + // 10점 만점을 5점 만점으로 환산 + min(max(rating / 2.0, 0), Double(maxStars)) + } + + var body: some View { + HStack { + HStack(spacing: 2) { + ForEach(0.. CGFloat { + let starValue = Double(index) + let nextStarValue = Double(index + 1) + + if normalizedRating >= nextStarValue { + // 완전히 채워진 별 + return totalWidth + } else if normalizedRating > starValue { + // 부분적으로 채워진 별 + let fraction = normalizedRating - starValue + return totalWidth * fraction + } else { + // 빈 별 + return 0 + } + } +} + +#Preview { + VStack(spacing: 20) { + StarRatingView(rating: 8.7) + + StarRatingView(rating: 7.5) + + StarRatingView(rating: 9.2) + + StarRatingView(rating: 5.0) + + StarRatingView(rating: 10.0) + } + .padding() +} diff --git a/MovieBooking/Feature/MovieDetail/MovieDetailView.swift b/MovieBooking/Feature/MovieDetail/MovieDetailView.swift new file mode 100644 index 0000000..b99d607 --- /dev/null +++ b/MovieBooking/Feature/MovieDetail/MovieDetailView.swift @@ -0,0 +1,18 @@ +// +// MovieDetailView.swift +// MovieBooking +// +// Created by 홍석현 on 10/16/25. +// + +import SwiftUI + +struct MovieDetailView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + MovieDetailView() +} From f74b4ea42d32ed2e753e827ac2a6b82217cc476a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=92=E1=85=A9=E1=86=BC=E1=84=89=E1=85=A5=E1=86=A8?= =?UTF-8?q?=E1=84=92=E1=85=A7=E1=86=AB?= Date: Fri, 17 Oct 2025 19:11:31 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat.=20=EC=98=81=ED=99=94=20=ED=8F=AC?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=20=EB=B7=B0=20=EB=B0=8F=20MovieDetailView=20?= =?UTF-8?q?UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/MoviePosterView.swift | 40 +++++++++++++++++++ .../Feature/MovieDetail/MovieDetailView.swift | 13 +++++- 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift diff --git a/MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift b/MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift new file mode 100644 index 0000000..8e0b319 --- /dev/null +++ b/MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift @@ -0,0 +1,40 @@ +// +// MoviePosterView.swift +// MovieBooking +// +// Created by 홍석현 on 10/16/25. +// + +import SwiftUI + +struct MoviePosterView: View { + let posterPath: String + + var body: some View { + AsyncImage(url: URL(string: "https://image.tmdb.org/t/p/w500\(posterPath)")) { phase in + switch phase { + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fill) + case .failure: + Color.red + .overlay( + Text("Failed to load image") + .foregroundColor(.white) + ) + case .empty: + ProgressView() + @unknown default: + EmptyView() + } + } + .frame(width: 250, height: 250) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: .black.opacity(0.4), radius: 10, x: 0, y: 8) + } +} + +#Preview { + MoviePosterView(posterPath: "/bUrReoZFLGti6ehkBW0xw8f12MT.jpg") +} diff --git a/MovieBooking/Feature/MovieDetail/MovieDetailView.swift b/MovieBooking/Feature/MovieDetail/MovieDetailView.swift index b99d607..a591ac0 100644 --- a/MovieBooking/Feature/MovieDetail/MovieDetailView.swift +++ b/MovieBooking/Feature/MovieDetail/MovieDetailView.swift @@ -8,11 +8,20 @@ import SwiftUI struct MovieDetailView: View { + let posterPath: String + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + ScrollView { + VStack(spacing: 24) { + MoviePosterView(posterPath: posterPath) + + MovieDetailCardView(model: .mockData) + .padding(.horizontal, 24) + } + } } } #Preview { - MovieDetailView() + MovieDetailView(posterPath: "/bUrReoZFLGti6ehkBW0xw8f12MT.jpg") } From 85f3832bbba5a76f0998d87b0c413d2139137655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=92=E1=85=A9=E1=86=BC=E1=84=89=E1=85=A5=E1=86=A8?= =?UTF-8?q?=E1=84=92=E1=85=A7=E1=86=AB?= Date: Fri, 17 Oct 2025 19:17:41 +0900 Subject: [PATCH 3/4] chor. --- MovieBooking/Domain/Entity/MovieDetail.swift | 2 +- .../Components/MoviePosterView.swift | 6 +++++- .../Feature/MovieDetail/MovieDetailView.swift | 20 +++++++++---------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/MovieBooking/Domain/Entity/MovieDetail.swift b/MovieBooking/Domain/Entity/MovieDetail.swift index f8f0d11..8dd01cf 100644 --- a/MovieBooking/Domain/Entity/MovieDetail.swift +++ b/MovieBooking/Domain/Entity/MovieDetail.swift @@ -45,7 +45,7 @@ extension MovieDetail { releaseDate: "1994-9-23.", runningTime: 142 * 60, rating: 8.7, - posterPath: nil, + posterPath: "/bUrReoZFLGti6ehkBW0xw8f12MT.jpg", summary: "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency." ) } diff --git a/MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift b/MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift index 8e0b319..79a3c2a 100644 --- a/MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift +++ b/MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift @@ -8,7 +8,11 @@ import SwiftUI struct MoviePosterView: View { - let posterPath: String + private let posterPath: String + + init(posterPath: String) { + self.posterPath = posterPath + } var body: some View { AsyncImage(url: URL(string: "https://image.tmdb.org/t/p/w500\(posterPath)")) { phase in diff --git a/MovieBooking/Feature/MovieDetail/MovieDetailView.swift b/MovieBooking/Feature/MovieDetail/MovieDetailView.swift index a591ac0..787aa38 100644 --- a/MovieBooking/Feature/MovieDetail/MovieDetailView.swift +++ b/MovieBooking/Feature/MovieDetail/MovieDetailView.swift @@ -8,20 +8,20 @@ import SwiftUI struct MovieDetailView: View { - let posterPath: String - - var body: some View { - ScrollView { - VStack(spacing: 24) { - MoviePosterView(posterPath: posterPath) - - MovieDetailCardView(model: .mockData) - .padding(.horizontal, 24) + var body: some View { + ScrollView { + VStack(spacing: 24) { + if let path = MovieDetail.mockData.posterPath { + MoviePosterView(posterPath: path) } + + MovieDetailCardView(model: .mockData) + .padding(.horizontal, 24) } } + } } #Preview { - MovieDetailView(posterPath: "/bUrReoZFLGti6ehkBW0xw8f12MT.jpg") + MovieDetailView() } From b6ca7dbc363d17a68eb2e829c86d883ab3a15f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=92=E1=85=A9=E1=86=BC=E1=84=89=E1=85=A5=E1=86=A8?= =?UTF-8?q?=E1=84=92=E1=85=A7=E1=86=AB?= Date: Fri, 17 Oct 2025 20:54:15 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chor.=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/MovieDetail/Components/GenreLabel.swift | 6 +++++- .../MovieDetail/Components/MovieDetailCardView.swift | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift b/MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift index b64836e..2918d9e 100644 --- a/MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift +++ b/MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift @@ -8,7 +8,11 @@ import SwiftUI struct GenreLabel: View { - let genre: Genre + private let genre: Genre + + init(genre: Genre) { + self.genre = genre + } var body: some View { Text(genre.name) diff --git a/MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift b/MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift index 3bc19de..4c75345 100644 --- a/MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift +++ b/MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift @@ -46,9 +46,9 @@ struct MovieDetailCardView: View { Text("예매하기") .frame(maxWidth: .infinity) .font(.system(size: 16, weight: .bold)) - .foregroundStyle(Color.white) + .foregroundStyle(.white) + .background(.basicPurple) .padding(.vertical, 16) - .background(Color.basicPurple) .clipShape(Capsule()) .shadow(color: .black.opacity(0.2), radius: 5, y: 5) }