diff --git a/APIService/Sources/SearchAPISupport/Mocks/SearchProductResponse.json b/APIService/Sources/SearchAPISupport/Mocks/SearchProductResponse.json index c52f8e6..db3acb0 100644 --- a/APIService/Sources/SearchAPISupport/Mocks/SearchProductResponse.json +++ b/APIService/Sources/SearchAPISupport/Mocks/SearchProductResponse.json @@ -16,7 +16,7 @@ "name": "롯데)펩시콜라캔355ML", "img": "https://image.woodongs.com/imgsvr/item/GD_8801056150013_007.jpg", "price": 1900, - "store": "GS25", + "store": "7-ELEVEn", "tag": "1+1", "proinfo": 0, "date": "2024-02-01T00:00:00Z", diff --git a/PyeonHaeng-iOS.xcodeproj/project.pbxproj b/PyeonHaeng-iOS.xcodeproj/project.pbxproj index 797cc53..b53dfe1 100644 --- a/PyeonHaeng-iOS.xcodeproj/project.pbxproj +++ b/PyeonHaeng-iOS.xcodeproj/project.pbxproj @@ -20,7 +20,6 @@ BA28F1882B6155910052855E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1872B6155910052855E /* HomeView.swift */; }; BA28F18B2B6155BD0052855E /* ProductInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F18A2B6155BD0052855E /* ProductInfoView.swift */; }; BA28F18E2B6156420052855E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F18D2B6156420052855E /* SettingsView.swift */; }; - BA28F1912B61566E0052855E /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1902B61566E0052855E /* SearchView.swift */; }; BA28F19D2B61572A0052855E /* Pretendard-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA28F1932B61572A0052855E /* Pretendard-Bold.otf */; }; BA28F19E2B61572A0052855E /* Pretendard-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA28F1942B61572A0052855E /* Pretendard-SemiBold.otf */; }; BA28F1A02B61572A0052855E /* Pretendard-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA28F1962B61572A0052855E /* Pretendard-Regular.otf */; }; @@ -48,6 +47,8 @@ BAE159DE2B663A9A002DCF94 /* HomeProductSorterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE159DD2B663A9A002DCF94 /* HomeProductSorterView.swift */; }; BAF2BEB32B61236100931AF0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = BAF2BEB22B61236100931AF0 /* Localizable.xcstrings */; }; E50176262B6A204F0098D1BE /* ProductInfoLineGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */; }; + E5028D5C2B96BA9400B36C16 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1902B61566E0052855E /* SearchView.swift */; }; + E5028D5D2B96BA9F00B36C16 /* ProductInfoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F2EC3F2B637D4A00EE0838 /* ProductInfoDetailView.swift */; }; E50584532B763C8C002FDACF /* ProductInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */; }; E52F371B2B947DC8000EBAD5 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */; }; E52F371D2B94D239000EBAD5 /* SearchViewComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */; }; @@ -59,7 +60,6 @@ E57F2AA42B7717EA00E12B3D /* ProductInfoAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E57F2AA32B7717EA00E12B3D /* ProductInfoAPI */; settings = {ATTRIBUTES = (Required, ); }; }; E57F2AA62B7717EA00E12B3D /* ProductInfoAPISupport in Frameworks */ = {isa = PBXBuildFile; productRef = E57F2AA52B7717EA00E12B3D /* ProductInfoAPISupport */; }; E57F2AA82B774CA700E12B3D /* ProductInfoDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = E57F2AA72B774CA700E12B3D /* ProductInfoDependency.swift */; }; - E5F2EC402B637D4A00EE0838 /* ProductInfoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F2EC3F2B637D4A00EE0838 /* ProductInfoDetailView.swift */; }; E5F2EC452B64926100EE0838 /* PromotionTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F2EC442B64926100EE0838 /* PromotionTagView.swift */; }; /* End PBXBuildFile section */ @@ -558,9 +558,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E5028D5D2B96BA9F00B36C16 /* ProductInfoDetailView.swift in Sources */, + E5028D5C2B96BA9400B36C16 /* SearchView.swift in Sources */, BAE159DA2B65FC35002DCF94 /* HomeProductListView.swift in Sources */, - E5F2EC402B637D4A00EE0838 /* ProductInfoDetailView.swift in Sources */, - E5F2EC402B637D4A00EE0838 /* ProductInfoDetailView.swift in Sources */, BA402F7E2B85E31800E86AAD /* ConvenienceSelectBottomSheetView.swift in Sources */, BA28F1852B6155810052855E /* OnboardingView.swift in Sources */, BAB5CF272B6B7CF3008B24BF /* HomeViewModel.swift in Sources */, @@ -570,8 +570,6 @@ E5462C662B65677B00E9FDF2 /* PromotionTag.swift in Sources */, E50584532B763C8C002FDACF /* ProductInfoViewModel.swift in Sources */, BAB720342B9325F200C2CA1A /* PromotionSelectBottomSheetView.swift in Sources */, - BA28F1912B61566E0052855E /* ProductSearchView.swift in Sources */, - BA28F1912B61566E0052855E /* SearchView.swift in Sources */, 9CE4B4732B6F0BA3002DC446 /* OnboardingViewModel.swift in Sources */, 9CE4B4752B6F78E8002DC446 /* OnboardingPageControl.swift in Sources */, BA28F18E2B6156420052855E /* SettingsView.swift in Sources */, diff --git a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchListCardView.swift b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchListCardView.swift index 563808c..5f0b926 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchListCardView.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchListCardView.swift @@ -5,24 +5,31 @@ // Created by 김응철 on 3/1/24. // +import Entity import SwiftUI // MARK: - SearchListCardView struct SearchListCardView: View { + let product: SearchProduct + var body: some View { HStack { - SearchImageView() - SearchDetailView() + SearchImageView(product: product) + SearchDetailView(product: product) } + .padding(.vertical, Metrics.verticalPadding) + .padding(.horizontal, Metrics.horizontalPadding) } } // MARK: - SearchImageView private struct SearchImageView: View { + let product: SearchProduct + var body: some View { - AsyncImage(url: nil) { phase in + AsyncImage(url: product.imageURL) { phase in if let image = phase.image { image .resizable() @@ -38,49 +45,47 @@ private struct SearchImageView: View { } .frame(width: Metrics.totalImageSize, height: Metrics.totalImageSize) } - - enum Metrics { - static let imageSize = 70.0 - static let totalImageSize = 96.0 - } } // MARK: - SearchDetailView private struct SearchDetailView: View { + let product: SearchProduct + var body: some View { - VStack(alignment: .leading) { - PromotionTagView(promotion: .buyOneGetOneFree) - .padding(.bottom, Metrics.promotionTagViewBottomPadding) - Text(verbatim: "펩시 제로 라임 250ml") + VStack(alignment: .leading, spacing: 8.0) { + PromotionTagView(promotion: product.promotion) + Text(product.name) .font(.title1) .foregroundStyle(.gray900) - .frame(maxWidth: .infinity, minHeight: 19, maxHeight: 19, alignment: .leading) - .padding(.bottom, Metrics.productTitleBottomPadding) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 8.0) HStack { - Text(verbatim: "1,800원") + Text("\(product.price.formatted())원") .font(.x2) .strikethrough() .foregroundColor(.gray100) - .padding(.trailing, Metrics.previousPriceTrailingPadding) - Text(verbatim: "개당") + Text("개당") .font(.c3) .foregroundColor(.gray900) - Text(verbatim: "1,250원") + Text("\(Int(product.price / 2).formatted())원") .font(.h4) .foregroundColor(.gray900) } .frame(maxWidth: .infinity, alignment: .trailing) } } - - enum Metrics { - static let promotionTagViewBottomPadding = 8.0 - static let productTitleBottomPadding = 16.0 - static let previousPriceTrailingPadding = 10.0 - } } -#Preview { - SearchListCardView() +// MARK: - Metrics + +private enum Metrics { + static let productTitleBottomPadding = 16.0 + static let previousPriceTrailingPadding = 10.0 + + static let verticalPadding = 20.0 + static let horizontalPadding = 16.0 + + static let imageSize = 70.0 + static let totalImageSize = 96.0 } diff --git a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift index c6108f9..e16bcde 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift @@ -6,6 +6,7 @@ // import DesignSystem +import Entity import SwiftUI // MARK: - SearchView @@ -19,26 +20,42 @@ struct SearchView: View where ViewModel: SearchViewModelRepresentable var body: some View { ScrollView { - LazyVStack { - Section { - SearchListCardView() - } header: { - SearchHeaderView() + LazyVStack(spacing: .zero) { + ForEach(Array(viewModel.state.products), id: \.key) { key, items in + Section { + ForEach(items) { item in + SearchListCardView(product: item) + } + } header: { + SearchHeaderView( + store: key, + productsCount: items.count + ) + .padding(.horizontal, Metrics.horizontalPadding) + .padding(.top, Metrics.headerTopPadding) + } footer: { + Rectangle() + .foregroundStyle(.gray050) + .frame(maxWidth: .infinity, maxHeight: 10) + } } } } + .scrollIndicators(.hidden) .toolbar { ToolbarItem(placement: .principal) { - SearchTextField() + SearchTextField() } } + .environmentObject(viewModel) } } // MARK: - SearchTextField -private struct SearchTextField: View { +private struct SearchTextField: View where ViewModel: SearchViewModelRepresentable { @State private var textInput: String = "" + @EnvironmentObject private var viewModel: ViewModel var body: some View { ZStack { @@ -55,6 +72,7 @@ private struct SearchTextField: View { lineWidth: Metrics.textFieldBorderWidth ) } + .onSubmit { viewModel.trigger(.textChanged(textInput)) } Button(action: { textInput = "" }) { @@ -70,13 +88,35 @@ private struct SearchTextField: View { // MARK: - SearchHeaderView private struct SearchHeaderView: View { + let store: ConvenienceStore + let productsCount: Int + var body: some View { HStack(spacing: 8.0) { - Image._7Eleven - Text(verbatim: "3") + convenienceImageView() + .resizable() + .scaledToFit() + .frame(height: 32.0) + Text(verbatim: "\(productsCount)") .font(.title2) + .foregroundStyle(.green500) + } + .frame(maxWidth: .infinity, alignment: .bottomLeading) + } + + private func convenienceImageView() -> Image { + switch store { + case .cu: + .cu + case .gs25: + .gs25 + case ._7Eleven: + ._7Eleven + case .emart24: + .emart24 + case .ministop: + .ministop } - .frame(maxWidth: .infinity, alignment: .leading) } } @@ -92,5 +132,8 @@ private enum Metrics { static let textFieldBorderWidth = 1.0 static let cornerRadius = 8.0 + static let horizontalPadding = 20.0 + static let headerTopPadding = 24.0 + static let removeButtonSize = 32.0 } diff --git a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchViewModel.swift b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchViewModel.swift index 9039ec0..0068a90 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchViewModel.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchViewModel.swift @@ -21,7 +21,7 @@ enum SearchAction { struct SearchState { var currentText = "" - var products = [SearchProduct]() + var products = [ConvenienceStore: [SearchProduct]]() var offset = 0 var hasMore = false @@ -101,10 +101,11 @@ final class SearchViewModel: SearchViewModelRepresentable { state.hasMore = paginatedModel.hasMore state.offset += 1 + + let results = Dictionary(grouping: paginatedModel.results, by: { $0.convenienceStore }) + if isReplace { - state.products = paginatedModel.results - } else { - state.products.append(contentsOf: paginatedModel.results) + state.products = results } } }