Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions MovieBooking/Data/DTO/Response/MovieDetailResponseDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
51 changes: 51 additions & 0 deletions MovieBooking/Domain/Entity/MovieDetail.swift
Original file line number Diff line number Diff line change
@@ -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: "/bUrReoZFLGti6ehkBW0xw8f12MT.jpg",
summary: "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency."
)
}
34 changes: 34 additions & 0 deletions MovieBooking/Feature/MovieDetail/Components/GenreLabel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// GenreLabel.swift
// MovieBooking
//
// Created by ν™μ„ν˜„ on 10/17/25.
//

import SwiftUI

struct GenreLabel: View {
private let genre: Genre

init(genre: Genre) {
self.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"))
}
128 changes: 128 additions & 0 deletions MovieBooking/Feature/MovieDetail/Components/MovieDetailCardView.swift
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

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

@Peter1119 λ­”κ°€ ν°νŠΈλ„ μ–΄λ–€κ±Έ μ“Έμ§€ 저희 μ •ν•˜λ©΄ 쒋을거 κ°™μ•„μš”!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

λ„΅ 쒋을 것 κ°™μ•„μš” !

.foregroundStyle(.white)
.background(.basicPurple)
.padding(.vertical, 16)
.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()
}
44 changes: 44 additions & 0 deletions MovieBooking/Feature/MovieDetail/Components/MoviePosterView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// MoviePosterView.swift
// MovieBooking
//
// Created by ν™μ„ν˜„ on 10/16/25.
//

import SwiftUI

struct MoviePosterView: View {
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
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")
}
86 changes: 86 additions & 0 deletions MovieBooking/Feature/MovieDetail/Components/StarRatingView.swift
Original file line number Diff line number Diff line change
@@ -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..<maxStars, id: \.self) { index in
ZStack {
// λ°°κ²½ 별 (νšŒμƒ‰)
Image(systemName: "star.fill")
.foregroundColor(.gray.opacity(0.3))

// μ±„μ›Œμ§„ 별 (λ…Έλž€μƒ‰)
Image(systemName: "star.fill")
.foregroundColor(.yellow)
.mask(
GeometryReader { geometry in
let fillWidth = calculateFillWidth(
for: index,
totalWidth: geometry.size.width
)
Rectangle()
.frame(width: fillWidth)
}
)
}
}
}

Text(String(format: "%.1f", normalizedRating))
.foregroundColor(.primary)
.font(.system(size: 16, weight: .light))
}
}

private func calculateFillWidth(for index: Int, totalWidth: CGFloat) -> 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()
}
27 changes: 27 additions & 0 deletions MovieBooking/Feature/MovieDetail/MovieDetailView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// MovieDetailView.swift
// MovieBooking
//
// Created by ν™μ„ν˜„ on 10/16/25.
//

import SwiftUI

struct MovieDetailView: View {
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()
}