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
2 changes: 1 addition & 1 deletion AppleStoreProductKiosk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
7FB949782E7BB47600E5F3CF /* AppleStoreProductKiosk.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = file; path = AppleStoreProductKiosk.xctestplan; sourceTree = "<group>"; };
7FB949782E7BB47600E5F3CF /* AppleStoreProductKiosk.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AppleStoreProductKiosk.xctestplan; sourceTree = "<group>"; };
C8F725DE2E790F9F00C2A1DE /* AppleStoreProductKiosk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppleStoreProductKiosk.app; sourceTree = BUILT_PRODUCTS_DIR; };
C8F725EB2E790FA100C2A1DE /* AppleStoreProductKioskTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppleStoreProductKioskTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public struct ProductListFeature {
var cartButtonState: CartButtonFeature.State

@Presents var alert: AlertState<Action.Alert>?
@Presents var cart: CartFeature.State?

init(selectedProducts: Shared<[Product]>) {
self._selectedProducts = selectedProducts
Expand Down Expand Up @@ -69,6 +70,7 @@ public struct ProductListFeature {
@CasePathable
public enum ScopeAction: Equatable {
case cardButton(CartButtonFeature.Action)
case cart(PresentationAction<CartFeature.Action>)
}

@CasePathable
Expand Down Expand Up @@ -116,6 +118,10 @@ public struct ProductListFeature {
Scope(state: \.cartButtonState, action: \.scope.cardButton) {
CartButtonFeature()
}

.ifLet(\.$cart, action: \.scope.cart) {
CartFeature()
}
}
}

Expand Down Expand Up @@ -166,7 +172,13 @@ extension ProductListFeature {
) -> Effect<Action> {
switch action {
case .cardButton(let action):
guard case .view(.onTap) = action else { return .none }
switch action {
case .view(.onTap):
state.cart = CartFeature.State(selectedProducts: state.$selectedProducts)
return .none
}

case .cart:
return .none
}
}
Expand Down
77 changes: 45 additions & 32 deletions AppleStoreProductKiosk/Features/Main/Feature/ProductListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,44 +29,24 @@ public struct ProductListView: View {
}
}

private var columns: [GridItem] {
Array(repeating: GridItem(.flexible(), spacing: 16), count: adaptiveColumnCount)
}

public var body: some View {
WithPerceptionTracking {
VStack {
Text("Apple Store")
.font(.largeTitle)
.fontWeight(.bold)
Text("원하는 제품을 선택하고 주문하세요.")
.font(.subheadline)
.foregroundStyle(Color.secondary)
headerView

SegmentsView(
items: store.productCategories
.map {
SegmentData(
id: $0.id,
title: $0.name,
icon: $0.name
)
},
items: store.productCategories.map {
SegmentData(id: $0.id, title: $0.name, icon: $0.name)
},
selectedID: $store.currentSelectedCategoryId
)

ScrollView {
LazyVGrid(
columns: Array(
repeating: GridItem(.flexible(), spacing: 16),
count: adaptiveColumnCount
),
spacing: 16
) {
ForEach(store.currentItems) { product in
ProductCardView(product: product) { id in
send(.onTapAddItem(id: id))
}
}
}
.padding(8)

productGrid
Spacer()
}
.scrollIndicators(.hidden)
Expand All @@ -83,10 +63,43 @@ public struct ProductListView: View {
.padding()
}
}
.onAppear {
send(.onAppear)
}
.onAppear { send(.onAppear) }
.alert($store.scope(state: \.alert, action: \.view.alert))
.cartSheet(store: store)
}
}
}

private extension ProductListView {
Copy link
Contributor

Choose a reason for hiding this comment

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

@minneee 완전 깔끔 해졌네요

var headerView: some View {
VStack {
Text("Apple Store")
.font(.largeTitle)
.fontWeight(.bold)
Text("원하는 제품을 선택하고 주문하세요.")
.font(.subheadline)
.foregroundStyle(Color.secondary)
}
}

var productGrid: some View {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(store.currentItems) { product in
ProductCardView(product: product) { id in
send(.onTapAddItem(id: id))
}
}
}
.padding(8)
}
}

private extension View {
func cartSheet(store: StoreOf<ProductListFeature>) -> some View {
self.sheet(
store: store.scope(state: \.$cart, action: \.scope.cart)
) { cartStore in
CartView(store: cartStore)
}
}
}
Expand Down
93 changes: 93 additions & 0 deletions AppleStoreProductKiosk/Features/ShoppingCart/CartFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// CartFeature.swift
// AppleStoreProductKiosk
//
// Created by 김민희 on 9/18/25.
//
import Foundation
import ComposableArchitecture

@Reducer
public struct CartFeature {
@ObservableState
public struct State: Equatable {
@Shared var selectedProducts: [Product]
@Presents var alert: AlertState<Action.Alert>?
}

@CasePathable
public enum Action: Equatable {
case decreaseQuantity(Product.ID)
case increaseQuantity(Product)
case removeProduct(Product.ID)
case clearCart
case checkoutButtonTapped
case alert(PresentationAction<Alert>)

@CasePathable
public enum Alert: Equatable {
case dismiss
}
}

public var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .decreaseQuantity(let id):
state.$selectedProducts.withLock { products in
guard let index = products.firstIndex(where: { $0.id == id }) else { return }
products.remove(at: index)
}
return .none

case .increaseQuantity(let product):
state.$selectedProducts.withLock { $0.append(product) }
return .none

case .removeProduct(let id):
state.$selectedProducts.withLock { products in
products.removeAll { $0.id == id }
}
return .none

case .clearCart:
state.$selectedProducts.withLock { $0.removeAll() }
return .none

case .checkoutButtonTapped:
guard !state.selectedProducts.isEmpty else {
state.alert = AlertState {
TextState("장바구니 비어 있음")
} actions: {
ButtonState(action: .dismiss) {
TextState("확인")
}
} message: {
TextState("상품을 선택해주세요")
}
return .none
}

let totalPrice = state.selectedProducts.reduce(0) { $0 + $1.price }
let formattedTotal = totalPrice.formatted(.currency(code: "KRW"))
state.alert = AlertState {
TextState("결제 완료")
} actions: {
ButtonState(action: .dismiss) {
TextState("확인")
}
} message: {
TextState("총 \(formattedTotal)의 주문이 접수되었습니다.\n감사합니다!")
}
state.$selectedProducts.withLock { $0.removeAll() }
return .none

case .alert(.dismiss):
return .none
case .alert:
return .none
}
}
.ifLet(\.$alert, action: \.alert)
}
}
84 changes: 67 additions & 17 deletions AppleStoreProductKiosk/Features/ShoppingCart/CartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,84 @@
//

import SwiftUI
import ComposableArchitecture

struct CartView: View {
@State var item = CartItem()
@Perception.Bindable var store: StoreOf<CartFeature>

public init(store: StoreOf<CartFeature>) {
self.store = store
}

var body: some View {
VStack(spacing: 0) {
CartHeaderView()
WithPerceptionTracking {
let items = makeCartItems(from: store.selectedProducts)

VStack(spacing: 0) {
CartHeaderView()

Divider()
Divider()

ScrollView {
VStack(spacing: 15) {
CartItemRowView(item: $item)
if items.isEmpty {
emptyStateView
} else {
ScrollView {
VStack(spacing: 15) {
ForEach(items) { item in
CartItemRowView(
item: item,
onTapDecrease: { store.send(.decreaseQuantity(item.product.id)) },
onTapIncrease: { store.send(.increaseQuantity(item.product)) },
onTapRemove: { store.send(.removeProduct(item.product.id)) }
)
}
}
.padding(.horizontal, 20)
.padding(.vertical, 30)
}
.scrollIndicators(.hidden)
}
.padding(.horizontal, 20)
.padding(.vertical, 30)
}
.scrollIndicators(.hidden)

Divider()
Divider()

CartSummaryView(item: $item)
CartSummaryView(
totalQuantity: items.reduce(0) { $0 + $1.quantity },
totalPrice: items.reduce(0) { $0 + $1.subtotal }
)
.padding(.vertical, 20)

CartActionButtonsView()
CartActionButtonsView(
onTapCancel: { store.send(.clearCart) },
onTapCheckout: { store.send(.checkoutButtonTapped) }
)
}
}
.alert($store.scope(state: \.alert, action: \.alert))
}
}

#Preview {
CartView()
private var emptyStateView: some View {
VStack(spacing: 12) {
Image(systemName: "cart")
.font(.system(size: 36, weight: .regular))
.foregroundStyle(.gray.opacity(0.6))
Text("장바구니가 비어 있어요.")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(.gray)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.vertical, 60)
}

private func makeCartItems(from products: [Product]) -> [CartItem] {
Dictionary(grouping: products, by: \.id)
.compactMap { _, items in
guard let product = items.first else { return nil }
return CartItem(product: product, quantity: items.count)
}
.sorted { $0.product.name < $1.product.name }
}
}

//#Preview {
// CartView()
//}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
import SwiftUI

struct CartActionButtonsView: View {
let onTapCancel: () -> Void
let onTapCheckout: () -> Void

var body: some View {
Grid(horizontalSpacing: 12) {
GridRow {
Button {
//전체 취소
} label: {
Button(action: onTapCancel) {
Text("전체 취소")
.font(.system(size: 15, weight: .regular))
.foregroundStyle(.black)
Expand All @@ -25,10 +26,8 @@ struct CartActionButtonsView: View {
}
}
.gridCellColumns(4)

Button {
//결제하기
} label: {

Button(action: onTapCheckout) {
Text("결제하기")
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(.white)
Expand All @@ -44,6 +43,6 @@ struct CartActionButtonsView: View {
}
}

#Preview {
CartActionButtonsView()
}
#Preview {
CartActionButtonsView(onTapCancel: {}, onTapCheckout: {})
}
Loading
Loading