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
125 changes: 125 additions & 0 deletions MovieBooking/Feature/MovieBook/Components/BookingOptionsCard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// BookingOptionsCard.swift
// MovieBooking
//
// Created by 홍석현 on 10/17/25.
//

import SwiftUI

struct BookingOptionsCard: View {
let theaters: [String]
let times: [String]
@Binding var selectedTheater: String
@Binding var selectedTime: String
@Binding var numberOfPeople: Int

var body: some View {
VStack(spacing: 20) {
// 극장 선택
CustomPickerRow(
title: "극장",
selection: $selectedTheater,
options: theaters
)

// 상영시간 선택
CustomPickerRow(
title: "상영시간",
selection: $selectedTime,
options: times
)

// 인원수 선택
CustomPickerRow(
title: "인원수",
selection: $numberOfPeople,
options: Array(1...10),
displayFormatter: { "\($0)명" }
)
}
.padding(20)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
}
}

// MARK: - Custom Picker Row
fileprivate struct CustomPickerRow<T: Hashable>: View {
let title: String
@Binding var selection: T
let options: [T]
var displayFormatter: ((T) -> String)?

var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.system(size: 16, weight: .semibold))

Menu {
ForEach(options, id: \.self) { option in
Button {
selection = option
} label: {
HStack {
Text(displayText(for: option))
if selection == option {
Spacer()
Image(systemName: "checkmark")
.foregroundColor(.basicPurple)
}
}
}
}
} label: {
HStack {
Text(displayText(for: selection))
.foregroundColor(.primary)
.font(.system(size: 16))

Spacer()

Image(systemName: "chevron.down")
.font(.system(size: 14))
.foregroundColor(.secondary)
}
.padding(14)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 14))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
}
}
}

private func displayText(for option: T) -> String {
if let formatter = displayFormatter {
return formatter(option)
}
return "\(option)"
}
}

#Preview {
BookingOptionsCardPreview()
}

private struct BookingOptionsCardPreview: View {
@State var selectedTheater = "CGV 강남"
@State var selectedTime = "14:30"
@State var numberOfPeople = 1

var body: some View {
BookingOptionsCard(
theaters: ["CGV 강남", "메가박스 코엑스", "롯데시네마 월드타워", "CGV 용산"],
times: ["10:00", "12:30", "14:30", "17:00", "19:30", "22:00"],
selectedTheater: $selectedTheater,
selectedTime: $selectedTime,
numberOfPeople: $numberOfPeople
)
.padding()
}
}
137 changes: 137 additions & 0 deletions MovieBooking/Feature/MovieBook/Components/PaymentInfoCard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//
// PaymentInfoCard.swift
// MovieBooking
//
// Created by 홍석현 on 10/17/25.
//

import SwiftUI

struct PaymentInfoCard: View {
let posterPath: String
let title: String
let pricePerTicket: Int
let numberOfPeople: Int
let onPaymentTapped: () -> Void

private var totalPrice: Int {
pricePerTicket * numberOfPeople
}

var body: some View {
VStack(spacing: 24) {
VStack(spacing: 16) {
// 헤더: 로고 + 결제 정보
HStack {
Image(systemName: "creditcard")
.font(.system(size: 24))
.foregroundColor(.basicPurple)

Text("결제 정보")
.font(.system(size: 18, weight: .light))

Spacer()
}

// 이미지 + 타이틀
HStack(alignment: .top, spacing: 16) {
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.gray
case .empty:
ProgressView()
@unknown default:
EmptyView()
}
}
.frame(width: 70, height: 70)
.clipShape(RoundedRectangle(cornerRadius: 16))

Text(title)
.font(.system(size: 18, weight: .light))
.lineLimit(2)

Spacer()
}

Divider()
.overlay(Color.white)

HStack {
Text("일반 관람권 (\(numberOfPeople)매)")

Spacer()

Text("\(pricePerTicket.formatted()) 원")
}
.font(.system(size: 16, weight: .light))
.foregroundColor(.secondary)

Divider()
.overlay(Color.white)

// 총 결제금액
HStack {
Text("총 결제금액")
.font(.system(size: 18, weight: .regular))

Spacer()

Text("\(totalPrice.formatted()) 원")
.font(.system(size: 24, weight: .regular))
.foregroundColor(.basicPurple)
}
}
.padding(20)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color.basicPurple.opacity(0.1),
Color.basicPurple.opacity(0.2),
Color.basicPurple.opacity(0.25),
Color.basicPurple.opacity(0.2),
Color.basicPurple.opacity(0.1)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.basicPurple.opacity(0.15), lineWidth: 0.5)
)
.shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 3)

// 결제하기 버튼
Button(action: onPaymentTapped) {
Text("결제하기")
.frame(maxWidth: .infinity)
.font(.system(size: 18, weight: .bold))
.foregroundStyle(Color.white)
.padding(.vertical, 16)
.background(Color.basicPurple)
.clipShape(Capsule())
}
}
}
}

#Preview {
PaymentInfoCard(
posterPath: "/bUrReoZFLGti6ehkBW0xw8f12MT.jpg",
title: "The Dark Knight",
pricePerTicket: 13000,
numberOfPeople: 2,
onPaymentTapped: {
print("결제하기 버튼 눌림")
}
)
.padding()
}
60 changes: 60 additions & 0 deletions MovieBooking/Feature/MovieBook/MovieBookView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// MovieBookView.swift
// MovieBooking
//
// Created by 홍석현 on 10/17/25.
//

import SwiftUI

struct MovieBookView: View {
let posterPath: String
let title: String
let pricePerTicket: Int = 13000

@State private var selectedTheater: String = "CGV 강남"
@State private var selectedTime: String = "14:30"
@State private var numberOfPeople: Int = 1

private let theaters = ["CGV 강남", "메가박스 코엑스", "롯데시네마 월드타워", "CGV 용산"]
private let times = ["10:00", "12:30", "14:30", "17:00", "19:30", "22:00"]

var body: some View {
ScrollView {
VStack(spacing: 24) {
// 극장/시간/인원 선택 카드
BookingOptionsCard(
theaters: theaters,
times: times,
selectedTheater: $selectedTheater,
selectedTime: $selectedTime,
numberOfPeople: $numberOfPeople
)
// 결제 정보 카드
PaymentInfoCard(
posterPath: posterPath,
title: title,
pricePerTicket: pricePerTicket,
numberOfPeople: numberOfPeople,
onPaymentTapped: handlePayment
)
}
.padding(24)
}
}

private func handlePayment() {
print("결제하기 버튼 눌림")
print("극장: \(selectedTheater)")
print("시간: \(selectedTime)")
print("인원: \(numberOfPeople)명")
print("총액: ₩\((pricePerTicket * numberOfPeople).formatted())")
}
}

#Preview {
MovieBookView(
posterPath: "/bUrReoZFLGti6ehkBW0xw8f12MT.jpg",
title: "The Dark Knight"
)
}
Loading