diff --git a/Koin/Apps/SceneDelegate.swift b/Koin/Apps/SceneDelegate.swift index bae4b242..2fd5ee68 100644 --- a/Koin/Apps/SceneDelegate.swift +++ b/Koin/Apps/SceneDelegate.swift @@ -347,7 +347,7 @@ extension SceneDelegate { let shareMenuListUseCase = DefaultShareMenuListUseCase(diningRepository: diningRepository) let fetchDiningNotiUseCase = DefaultFetchDiningNotiUseCase(notiRepository: notiReposotiry) let changeNotiUseCase = DefaultChangeNotiUseCase(notiRepository: notiReposotiry) - let fetchCurrentDiningItemsUseCase = DefaultFetchCurrentDiningItemsUseCase(repository: diningRepository) + let fetchOperatingDiningPlacesUseCase = DefaultFetchOperatingDiningPlacesUseCase(repository: diningRepository) let reportSoldoutDiningUseCase = DefaultReportSoldoutDiningUseCase(repository: diningRepository) let uploadFileUseCase = DefaultUploadFileUseCase(coreRepository: coreRepository) let assignAbTestUseCase = DefaultAssignAbTestUseCase(abTestRepository: abTestRepository) @@ -358,7 +358,7 @@ extension SceneDelegate { shareMenuListUseCase: shareMenuListUseCase, fetchDiningNotiUseCase: fetchDiningNotiUseCase, changeNotiUseCase: changeNotiUseCase, - fetchCurrentDiningItemsUseCase: fetchCurrentDiningItemsUseCase, + fetchOperatingDiningPlacesUseCase: fetchOperatingDiningPlacesUseCase, reportSoldoutDiningUseCase: reportSoldoutDiningUseCase, uploadFileUseCase: uploadFileUseCase, assignAbTestUseCase: assignAbTestUseCase, @@ -471,7 +471,7 @@ extension SceneDelegate { let shareMenuListUseCase = DefaultShareMenuListUseCase(diningRepository: diningRepository) let fetchDiningNotiUseCase = DefaultFetchDiningNotiUseCase(notiRepository: notiReposotiry) let changeNotiUseCase = DefaultChangeNotiUseCase(notiRepository: notiReposotiry) - let fetchCurrentDiningItemsUseCase = DefaultFetchCurrentDiningItemsUseCase(repository: diningRepository) + let fetchOperatingDiningPlacesUseCase = DefaultFetchOperatingDiningPlacesUseCase(repository: diningRepository) let reportSoldoutDiningUseCase = DefaultReportSoldoutDiningUseCase(repository: diningRepository) let uploadFileUseCase = DefaultUploadFileUseCase(coreRepository: coreRepository) let assignAbTestUseCase = DefaultAssignAbTestUseCase(abTestRepository: abTestRepository) @@ -482,7 +482,7 @@ extension SceneDelegate { shareMenuListUseCase: shareMenuListUseCase, fetchDiningNotiUseCase: fetchDiningNotiUseCase, changeNotiUseCase: changeNotiUseCase, - fetchCurrentDiningItemsUseCase: fetchCurrentDiningItemsUseCase, + fetchOperatingDiningPlacesUseCase: fetchOperatingDiningPlacesUseCase, reportSoldoutDiningUseCase: reportSoldoutDiningUseCase, uploadFileUseCase: uploadFileUseCase, assignAbTestUseCase: assignAbTestUseCase, diff --git a/Koin/Domain/Model/Dining/DiningPlace.swift b/Koin/Domain/Model/Dining/DiningPlace.swift index 3026af06..e35bdaf0 100644 --- a/Koin/Domain/Model/Dining/DiningPlace.swift +++ b/Koin/Domain/Model/Dining/DiningPlace.swift @@ -5,7 +5,7 @@ // Created by 김나훈 on 3/9/24. // Nullable 2024.04.26 -enum DiningPlace: Equatable { +enum DiningPlace: Equatable, CaseIterable { case cornerA case cornerB case cornerC diff --git a/Koin/Domain/Model/Dining/DiningReportRequest.swift b/Koin/Domain/Model/Dining/DiningReportRequest.swift new file mode 100644 index 00000000..3a1a1684 --- /dev/null +++ b/Koin/Domain/Model/Dining/DiningReportRequest.swift @@ -0,0 +1,14 @@ +// +// DiningReportRequest.swift +// koin +// +// Created by 홍기정 on 10/28/26. +// + +import Foundation + +struct DiningReportRequest { + let place: DiningPlace + let id: Int + let evidenceImageData: Data +} diff --git a/Koin/Domain/UseCase/Dining/Dining/FetchCurrentDiningItemsUseCase.swift b/Koin/Domain/UseCase/Dining/Dining/FetchCurrentDiningItemsUseCase.swift deleted file mode 100644 index 053acd8b..00000000 --- a/Koin/Domain/UseCase/Dining/Dining/FetchCurrentDiningItemsUseCase.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// FetchCurrentDiningItemsUseCase.swift -// koin -// -// Created by 홍기정 on 10/18/26. -// - -import Foundation - -protocol FetchCurrentDiningItemsUseCase { - func execute() async throws -> [DiningItem] -} - -final class DefaultFetchCurrentDiningItemsUseCase: FetchCurrentDiningItemsUseCase { - - let repository: DiningRepository - - init(repository: DiningRepository) { - self.repository = repository - } - - func execute() async throws -> [DiningItem] { - let calendar = Calendar.current - let hour = calendar.component(.hour, from: Date()) - let minute = calendar.component(.minute, from: Date()) - let weekday = calendar.component(.weekday, from: Date()) - let targetTime = [hour, minute].map { String($0) }.joined(separator: ":") - let dayOfWeek: DayOfWeek = switch weekday { - case 1: .sunday - case 7: .saturday - default: .weekday - } - - let mealType = try? await repository.fetchCoopShopList() - .firstValue() - .opens - .filter { - $0.dayOfWeek == dayOfWeek - } - .first { - $0.contains(time: targetTime) - } - .map { - $0.type.toDiningType() - } - - return try await repository.fetchDiningList(date: Date()).firstValue() - .filter { $0.type == mealType } - } -} diff --git a/Koin/Domain/UseCase/Dining/Dining/FetchOperatingDiningPlacesUseCase.swift b/Koin/Domain/UseCase/Dining/Dining/FetchOperatingDiningPlacesUseCase.swift new file mode 100644 index 00000000..62422e11 --- /dev/null +++ b/Koin/Domain/UseCase/Dining/Dining/FetchOperatingDiningPlacesUseCase.swift @@ -0,0 +1,72 @@ +// +// FetchOperatingDiningPlacesUseCase.swift +// koin +// +// Created by 홍기정 on 10/18/26. +// + +import Foundation + +protocol FetchOperatingDiningPlacesUseCase { + func execute() async throws -> [(place: DiningPlace, id: Int?)] +} + +final class DefaultFetchOperatingDiningPlacesUseCase: FetchOperatingDiningPlacesUseCase { + + let repository: DiningRepository + + init(repository: DiningRepository) { + self.repository = repository + } + + func execute() async throws -> [(place: DiningPlace, id: Int?)] { + let calendar = Calendar.current + let now = Date() + let dayOfWeek: DayOfWeek = { + let weekday = calendar.component(.weekday, from: now) + switch weekday { + case 1: + return .sunday + case 7: + return .saturday + default: + return.weekday + } + }() + let currentTime = { + let hour = calendar.component(.hour, from: now) + let minute = calendar.component(.minute, from: now) + return String(format: "%02d:%02d", hour, minute) + }() + let currentDiningType = try? await repository.fetchCoopShopList() + .firstValue() + .opens + .filter { + $0.dayOfWeek == dayOfWeek + } + .first { + $0.contains(time: currentTime) + } + .map { + $0.type.toDiningType() + } + let operatingDiningItems = try await repository.fetchDiningList(date: Date()).firstValue() + .filter { $0.type == currentDiningType } + .filter { $0.place != .secondCampus } + + let diningPlaces: [(DiningPlace, id: Int?)] = DiningPlace.allCases + .filter { + $0 != .secondCampus + } + .map { place in + let operatingDiningItem: DiningItem? = operatingDiningItems.first { + $0.place == place + && $0.isSoldOut == false + && $0.menu.first != "미운영" + } + return (place, operatingDiningItem?.id) + } + + return diningPlaces + } +} diff --git a/Koin/Presentation/Dining/Dining/DiningHostingController.swift b/Koin/Presentation/Dining/Dining/DiningHostingController.swift index e85a0f25..b635556c 100644 --- a/Koin/Presentation/Dining/Dining/DiningHostingController.swift +++ b/Koin/Presentation/Dining/Dining/DiningHostingController.swift @@ -134,17 +134,8 @@ extension DiningHostingController { } extension DiningHostingController { - private func onCompleteButtonTapped( - selectedItem: DiningItem, - evidenceImage: UIImage - ) { - guard let imageData = evidenceImage.jpegData(compressionQuality: 0.5) else { - return - } - rootView.didTapReport( - selectedItem: selectedItem, - evidenceImage: imageData - ) + private func onCompleteButtonTapped(_ request: DiningReportRequest) { + rootView.didTapReport(request) } } @@ -174,56 +165,65 @@ extension DiningHostingController { } private func showReportModal(selectedPlace: DiningPlace?) { - let diningItems = rootView.currentDiningItems - guard let selectedItem = diningItems.first(where: { $0.place == selectedPlace }) ?? diningItems.first else { + let operatingPlaces = rootView.operatingPlaces + guard operatingPlaces.contains(where: { $0.id != nil }) else { showToastMessage(message: "학생식당 운영시간이 아닙니다.") return } - self.modalView = makeModalView(selectedItem: selectedItem) + self.modalView = makeModalView( + operatingPlaces: operatingPlaces, + selectedPlace: selectedPlace + ) presentReportModalViewController() } - private func makeModalView(selectedItem: DiningItem) -> DiningReportModalView { + private func makeModalView( + operatingPlaces: [(place: DiningPlace, id: Int?)], + selectedPlace: DiningPlace? + ) -> DiningReportModalView { return DiningReportModalView( - diningItems: rootView.currentDiningItems, + operatingPlaces: operatingPlaces, onDismissButtonTapped: { [weak self] in guard let self else { return } - if let reportAbTestName, let selectedItem = modalView?.selectedItem { + if let reportAbTestName, let selectedPlace = modalView?.selectedPlace { rootView.makeLogAnalyticsEvent( label: EventParameter.EventLabel.Campus.Dining.cafeteriaDiningSoldoutExit, category: .click, - value: "\(reportAbTestName)_\(selectedItem.place.name)_이탈" + value: "\(reportAbTestName)_\(selectedPlace.name)_이탈" ) } dismiss(animated: true) }, - onPlaceSelected: { [weak self] item in + onPlaceSelected: { [weak self] place in guard let self, let reportAbTest = rootView.reportAbTest else { return } rootView.makeLogAnalyticsEvent( label: EventParameter.EventLabel.Campus.Dining.cafeteriaDiningSoldoutSelect, category: .select, - value: "\(reportAbTest.logValue)_\(item.place.name)" + value: "\(reportAbTest.logValue)_\(place.name)" ) }, onSelectImageButtonTapped: { [weak self] in guard let self else { return } - if let reportAbTestName, let selectedItem = modalView?.selectedItem { + if let reportAbTestName, let selectedPlace = modalView?.selectedPlace { rootView.makeLogAnalyticsEvent( label: EventParameter.EventLabel.Campus.Dining.cafeteriaDiningSoldoutPhoto, category: .click, - value: "\(reportAbTestName)_\(selectedItem.place.name)" + value: "\(reportAbTestName)_\(selectedPlace.name)" ) } checkAndPresentPicker() }, onCompleteButtonTapped: onCompleteButtonTapped, - selectedItem: selectedItem + showToastMessage: { [weak self] message in + self?.showToastMessage(message: message) + }, + selectedPlace: selectedPlace ) } diff --git a/Koin/Presentation/Dining/Dining/DiningView.swift b/Koin/Presentation/Dining/Dining/DiningView.swift index 0d535a11..ab91dbab 100644 --- a/Koin/Presentation/Dining/Dining/DiningView.swift +++ b/Koin/Presentation/Dining/Dining/DiningView.swift @@ -23,8 +23,8 @@ struct DiningView: ActionBindableView { // MARK: - Properties @State private var viewModel: DiningViewModel - var currentDiningItems: [DiningItem] { - viewModel.currentDiningItems + var operatingPlaces: [(place: DiningPlace, id: Int?)] { + viewModel.operatingPlaces } var reportAbTest: DiningReportAbTest? { viewModel.reportAbTest @@ -68,6 +68,7 @@ struct DiningView: ActionBindableView { } } .onChange(of: viewModel.didFinishReporting) { + IndicatorView.dismiss() sendAction(.didFinishReporting) } .onChange(of: viewModel.diningNoti) { @@ -93,12 +94,9 @@ struct DiningView: ActionBindableView { viewModel.execute(.didTapCloseReportTooltip) } - func didTapReport( - selectedItem: DiningItem, - evidenceImage imageData: Data - ) { + func didTapReport(_ request: DiningReportRequest) { IndicatorView.show() - viewModel.execute(.didTapReport(selectedItem: selectedItem, imageData: imageData)) + viewModel.execute(.didTapReport(request)) } func didTapSoldOutSwitch(isOn: Bool) { @@ -212,4 +210,3 @@ extension DiningView { ) } } - diff --git a/Koin/Presentation/Dining/Dining/DiningViewModel.swift b/Koin/Presentation/Dining/Dining/DiningViewModel.swift index 28eb247c..33505bbb 100644 --- a/Koin/Presentation/Dining/Dining/DiningViewModel.swift +++ b/Koin/Presentation/Dining/Dining/DiningViewModel.swift @@ -18,10 +18,7 @@ final class DiningViewModel: SwiftUIViewModelProtocol { case didShowToast case didTapCloseShareTooltip case didTapCloseReportTooltip - case didTapReport( - selectedItem: DiningItem, - imageData: Data - ) + case didTapReport(DiningReportRequest) case didTapShare(DiningItem) case didTapSoldOutSwitch(Bool) case didTapImageUploadSwitch(Bool) @@ -35,14 +32,14 @@ final class DiningViewModel: SwiftUIViewModelProtocol { private let shareMenuListUseCase: ShareMenuListUseCase private let fetchDiningNotiUseCase: FetchDiningNotiUseCase private let changeNotiUseCase: ChangeNotiUseCase - private let fetchCurrentDiningItemsUseCase: FetchCurrentDiningItemsUseCase + private let fetchOperatingDiningPlacesUseCase: FetchOperatingDiningPlacesUseCase private let reportSoldoutDiningUseCase: ReportSoldoutDiningUseCase private let uploadFileUseCase: UploadFileUseCase private let assignAbTestUseCase: AssignAbTestUseCase private let logAnalyticsEventUseCase: LogAnalyticsEventUseCase private var fetchDiningListTask: Task? - private var fetchCurrnetDiningItemsTask: Task? + private var fetchOperatingDiningPlacesTask: Task? private var assignReportABTestTask: Task? // MARK: - State @@ -50,7 +47,7 @@ final class DiningViewModel: SwiftUIViewModelProtocol { private(set) var selectedDate: Date = Date() private(set) var selectedDiningType: DiningType = .breakfast private(set) var diningLists: [DiningList] = [] - private(set) var currentDiningItems: [DiningItem] = [] + private(set) var operatingPlaces: [(place: DiningPlace, id: Int?)] = [] private(set) var toastMessage: String? private(set) var didFinishReporting: Int = 0 private(set) var shouldShowShareTooltip = !UserDefaults.standard.bool(forKey: "hasShownDiningShareTooltip") @@ -59,7 +56,7 @@ final class DiningViewModel: SwiftUIViewModelProtocol { private(set) var diningNoti: DiningNoti? var isLoading: Bool { fetchDiningListTask != nil - || fetchCurrnetDiningItemsTask != nil + || fetchOperatingDiningPlacesTask != nil || assignReportABTestTask != nil } @@ -73,7 +70,7 @@ final class DiningViewModel: SwiftUIViewModelProtocol { shareMenuListUseCase: ShareMenuListUseCase, fetchDiningNotiUseCase: FetchDiningNotiUseCase, changeNotiUseCase: ChangeNotiUseCase, - fetchCurrentDiningItemsUseCase: FetchCurrentDiningItemsUseCase, + fetchOperatingDiningPlacesUseCase: FetchOperatingDiningPlacesUseCase, reportSoldoutDiningUseCase: ReportSoldoutDiningUseCase, uploadFileUseCase: UploadFileUseCase, assignAbTestUseCase: AssignAbTestUseCase, @@ -86,7 +83,7 @@ final class DiningViewModel: SwiftUIViewModelProtocol { self.shareMenuListUseCase = shareMenuListUseCase self.fetchDiningNotiUseCase = fetchDiningNotiUseCase self.changeNotiUseCase = changeNotiUseCase - self.fetchCurrentDiningItemsUseCase = fetchCurrentDiningItemsUseCase + self.fetchOperatingDiningPlacesUseCase = fetchOperatingDiningPlacesUseCase self.reportSoldoutDiningUseCase = reportSoldoutDiningUseCase self.uploadFileUseCase = uploadFileUseCase self.assignAbTestUseCase = assignAbTestUseCase @@ -99,12 +96,12 @@ final class DiningViewModel: SwiftUIViewModelProtocol { switch input { case .onFirstAppear: fetchDiningList() - fetchCurrnetDiningItems() + fetchOperatingDiningPlaces() assignReportABTest() fetchNotiListIfNeeded() case .refresh: fetchDiningList() - fetchCurrnetDiningItems() + fetchOperatingDiningPlaces() case let .changeSelectedDate(date): changeSelectedDate(date) case let .changeSelectedDiningType(type): @@ -115,8 +112,8 @@ final class DiningViewModel: SwiftUIViewModelProtocol { hideShareTooltip() case .didTapCloseReportTooltip: hideReportTooltip() - case let .didTapReport(selectedItem, imageData): - postReport(selectedItem, imageData) + case let .didTapReport(request): + postReport(request) case let .didTapShare(selecteItem): shareMenuList(selectedItem: selecteItem) case let .didTapSoldOutSwitch(isOn): @@ -170,19 +167,19 @@ extension DiningViewModel { } } } - private func fetchCurrnetDiningItems() { - fetchCurrnetDiningItemsTask?.cancel() + private func fetchOperatingDiningPlaces() { + fetchOperatingDiningPlacesTask?.cancel() - fetchCurrnetDiningItemsTask = Task { + fetchOperatingDiningPlacesTask = Task { do { defer { if !Task.isCancelled { - fetchCurrnetDiningItemsTask = nil + fetchOperatingDiningPlacesTask = nil } } - let result = try await fetchCurrentDiningItemsUseCase.execute() + let operatingPlaces = try await fetchOperatingDiningPlacesUseCase.execute() guard !Task.isCancelled else { return } - currentDiningItems = result + self.operatingPlaces = operatingPlaces } catch { guard !Task.isCancelled else { return } toastMessage = (error as? ErrorResponse)?.message ?? error.localizedDescription @@ -247,17 +244,14 @@ extension DiningViewModel { } extension DiningViewModel { - private func postReport( - _ selectedItem: DiningItem, - _ imageData: Data - ) { + private func postReport(_ request: DiningReportRequest) { Task { do { defer { didFinishReporting += 1 } guard let imageUrl = try await uploadFileUseCase.execute( - files: [imageData], + files: [request.evidenceImageData], domain: .coop ) .firstValue() @@ -266,15 +260,15 @@ extension DiningViewModel { throw ErrorResponse.imageUploadError } try await reportSoldoutDiningUseCase.execute( - diningId: selectedItem.id, + diningId: request.id, imageUrl: imageUrl ) - toastMessage = "\(selectedItem.place.name) 품절을 제보했습니다." - makeReportLogEvent(category: .submit, selectedItem: selectedItem) + toastMessage = "\(request.place.name) 품절을 제보했습니다." + makeReportLogEvent(category: .submit, selectedPlace: request.place) } catch { guard !Task.isCancelled else { return } toastMessage = (error as? ErrorResponse)?.message ?? error.localizedDescription - makeReportLogEvent(category: .fail, selectedItem: selectedItem) + makeReportLogEvent(category: .fail, selectedPlace: request.place) } } } @@ -296,7 +290,7 @@ extension DiningViewModel { extension DiningViewModel { private func makeReportLogEvent( category: EventParameter.EventCategory, - selectedItem: DiningItem + selectedPlace: DiningPlace ) { guard let reportAbTestName = reportAbTest?.logValue else { return @@ -304,7 +298,7 @@ extension DiningViewModel { makeLogAnalyticsEvent( label: EventParameter.EventLabel.Campus.Dining.cafeteriaDiningSoldoutSubmit, category: category, - value: "\(reportAbTestName)_\(selectedItem.place.name)" + value: "\(reportAbTestName)_\(selectedPlace.name)" ) } diff --git a/Koin/Presentation/Dining/Dining/Subviews/DiningMenuListView.swift b/Koin/Presentation/Dining/Dining/Subviews/DiningMenuListView.swift index 8007e743..40cfe756 100644 --- a/Koin/Presentation/Dining/Dining/Subviews/DiningMenuListView.swift +++ b/Koin/Presentation/Dining/Dining/Subviews/DiningMenuListView.swift @@ -96,7 +96,7 @@ struct DiningMenuListView: View { ) .overlayReportTooltip( shouldShowReportTooltip: shouldShowReportTooltip, - onReportTooltipImageTapped: { onReportTapped(nil) }, + onReportTooltipImageTapped: { onReportTapped(list.items.first) }, onReportTooltipXButtonTapped: onCloseReportTooltipTapped ) } diff --git a/Koin/Presentation/Dining/Dining/Subviews/DiningReportModalView.swift b/Koin/Presentation/Dining/Dining/Subviews/DiningReportModalView.swift index a45c7131..eb284849 100644 --- a/Koin/Presentation/Dining/Dining/Subviews/DiningReportModalView.swift +++ b/Koin/Presentation/Dining/Dining/Subviews/DiningReportModalView.swift @@ -13,15 +13,13 @@ final class DiningReportModalView: UIView { // MARK: - Closures private let onDismissButtonTapped: ()->Void - private let onPlaceSelected: (DiningItem)->Void + private let onPlaceSelected: (DiningPlace)->Void private let onSelectImageButtonTapped: ()->Void - private let onCompleteButtonTapped: ( - _ selectedItem: DiningItem, - _ evidenceImage: UIImage - ) -> Void + private let onCompleteButtonTapped: (DiningReportRequest) -> Void + private let showToastMessage: (String) -> Void // MARK: - Properties - private let diningItems: [DiningItem] + private let operatingPlaces: [(place: DiningPlace, id: Int?)] private let evidenceImageDescriptions: [String] = [ "• 배식대의 품절 안내판 촬영", "• 학식당 앞 품절 식판 안내", @@ -31,7 +29,7 @@ final class DiningReportModalView: UIView { ] // MARK: - State - private(set) var selectedItem: DiningItem { + private(set) var selectedPlace: DiningPlace { didSet { updatePlaceButtons() updateCompleteButton() @@ -74,22 +72,27 @@ final class DiningReportModalView: UIView { // MARK: - Initializer init( - diningItems: [DiningItem], + operatingPlaces: [(place: DiningPlace, id: Int?)], onDismissButtonTapped: @escaping ()->Void, - onPlaceSelected: @escaping (DiningItem)->Void, + onPlaceSelected: @escaping (DiningPlace)->Void, onSelectImageButtonTapped: @escaping ()->Void, - onCompleteButtonTapped: @escaping ( - _ selectedItem: DiningItem, - _ evidenceImage: UIImage - ) -> Void, - selectedItem: DiningItem + onCompleteButtonTapped: @escaping (DiningReportRequest) -> Void, + showToastMessage: @escaping (String) -> Void, + selectedPlace: DiningPlace? ) { - self.diningItems = diningItems + // 호출부에서 제보 가능한 장소가 하나 이상임을 보장한다. + let initialPlace = operatingPlaces.first { $0.place == selectedPlace && $0.id != nil } + ?? operatingPlaces.first { $0.id != nil } + guard let initialPlace else { + preconditionFailure("품절 제보 가능한 식단이 없음") + } + self.operatingPlaces = operatingPlaces self.onDismissButtonTapped = onDismissButtonTapped self.onPlaceSelected = onPlaceSelected self.onSelectImageButtonTapped = onSelectImageButtonTapped self.onCompleteButtonTapped = onCompleteButtonTapped - self.selectedItem = selectedItem + self.showToastMessage = showToastMessage + self.selectedPlace = initialPlace.place super.init(frame: .zero) configureView() setAddTargets() @@ -110,9 +113,9 @@ final class DiningReportModalView: UIView { extension DiningReportModalView { // MARK: - Update private func updatePlaceButtons() { - zip(diningItems, placeButtons).forEach { diningItem, button in - let isSelected = diningItem == selectedItem - let textColor: ColorAsset = isSelected ? .new500 : .neutral500 + zip(operatingPlaces, placeButtons).forEach { diningPlace, button in + let isSelected = diningPlace.place == selectedPlace + let textColor: ColorAsset = diningPlace.id == nil ? .neutral300 : (isSelected ? .new500 : .neutral500) let borderColor: ColorAsset = isSelected ? .new500 : .neutral300 button.configuration?.attributedTitle?.foregroundColor = UIColor.appColor(textColor) button.configuration?.background.strokeColor = .appColor(borderColor) @@ -152,8 +155,10 @@ extension DiningReportModalView { guard let index = placeButtons.firstIndex(of: sender) else { return } - selectedItem = diningItems[index] - onPlaceSelected(diningItems[index]) + let option = operatingPlaces[index] + guard option.id != nil else { return } + selectedPlace = option.place + onPlaceSelected(option.place) } @objc private func didTapSelectImageButton() { @@ -168,10 +173,18 @@ extension DiningReportModalView { guard let evidenceImage else { return } - onCompleteButtonTapped( - selectedItem, - evidenceImage + guard let id = operatingPlaces.first(where: { $0.place == selectedPlace })?.id, + let imageData = evidenceImage.jpegData(compressionQuality: 0.5) else { + let error = ErrorResponse.unexpectedInternalError + showToastMessage(error.message) + return + } + let request = DiningReportRequest( + place: selectedPlace, + id: id, + evidenceImageData: imageData ) + onCompleteButtonTapped(request) } } @@ -179,10 +192,30 @@ extension DiningReportModalView { // MARK: - Configure private func configureView() { setUpStyles() + setUpButtons() setUpLayouts() setUpConstraints() } + private func setUpButtons() { + placeButtons = operatingPlaces.map { option in + UIButton().then { + var configuration = UIButton.Configuration.plain() + configuration.attributedTitle = AttributedString( + option.place.name, + attributes: AttributeContainer([ + .font: UIFont.appFont(.pretendardSemiBold, size: 14) + ]) + ) + configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12) + configuration.cornerStyle = .capsule + configuration.background.strokeWidth = 1 + $0.configuration = configuration + $0.isEnabled = option.id != nil + } + } + } + private func setUpStyles() { stackView.do { $0.axis = .vertical @@ -215,21 +248,6 @@ extension DiningReportModalView { $0.axis = .horizontal $0.spacing = 12 } - placeButtons = diningItems.map { diningItem in - UIButton().then { - var configuration = UIButton.Configuration.plain() - configuration.attributedTitle = AttributedString( - diningItem.place.name, - attributes: AttributeContainer([ - .font: UIFont.appFont(.pretendardSemiBold, size: 14) - ]) - ) - configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12) - configuration.cornerStyle = .capsule - configuration.background.strokeWidth = 1 - $0.configuration = configuration - } - } evidenceView.do { $0.axis = .vertical diff --git a/Koin/Presentation/Home/Category/CategoryHostingController.swift b/Koin/Presentation/Home/Category/CategoryHostingController.swift index 4d0d7635..f705acd1 100644 --- a/Koin/Presentation/Home/Category/CategoryHostingController.swift +++ b/Koin/Presentation/Home/Category/CategoryHostingController.swift @@ -116,7 +116,7 @@ extension CategoryHostingController { let shareMenuListUseCase = DefaultShareMenuListUseCase(diningRepository: diningRepository) let fetchDiningNotiUseCase = DefaultFetchDiningNotiUseCase(notiRepository: notiReposotiry) let changeNotiUseCase = DefaultChangeNotiUseCase(notiRepository: notiReposotiry) - let fetchCurrentDiningItemsUseCase = DefaultFetchCurrentDiningItemsUseCase(repository: diningRepository) + let fetchOperatingDiningPlacesUseCase = DefaultFetchOperatingDiningPlacesUseCase(repository: diningRepository) let reportSoldoutDiningUseCase = DefaultReportSoldoutDiningUseCase(repository: diningRepository) let uploadFileUseCase = DefaultUploadFileUseCase(coreRepository: coreRepository) let assignAbTestUseCase = DefaultAssignAbTestUseCase(abTestRepository: abTestRepository) @@ -127,7 +127,7 @@ extension CategoryHostingController { shareMenuListUseCase: shareMenuListUseCase, fetchDiningNotiUseCase: fetchDiningNotiUseCase, changeNotiUseCase: changeNotiUseCase, - fetchCurrentDiningItemsUseCase: fetchCurrentDiningItemsUseCase, + fetchOperatingDiningPlacesUseCase: fetchOperatingDiningPlacesUseCase, reportSoldoutDiningUseCase: reportSoldoutDiningUseCase, uploadFileUseCase: uploadFileUseCase, assignAbTestUseCase: assignAbTestUseCase, diff --git a/Koin/Presentation/Home/Home/HomeHostingController.swift b/Koin/Presentation/Home/Home/HomeHostingController.swift index d6e74d38..a0d509e2 100644 --- a/Koin/Presentation/Home/Home/HomeHostingController.swift +++ b/Koin/Presentation/Home/Home/HomeHostingController.swift @@ -65,7 +65,7 @@ extension HomeHostingController { let shareMenuListUseCase = DefaultShareMenuListUseCase(diningRepository: diningRepository) let fetchDiningNotiUseCase = DefaultFetchDiningNotiUseCase(notiRepository: notiReposotiry) let changeNotiUseCase = DefaultChangeNotiUseCase(notiRepository: notiReposotiry) - let fetchCurrentDiningItemsUseCase = DefaultFetchCurrentDiningItemsUseCase(repository: diningRepository) + let fetchOperatingDiningPlacesUseCase = DefaultFetchOperatingDiningPlacesUseCase(repository: diningRepository) let reportSoldoutDiningUseCase = DefaultReportSoldoutDiningUseCase(repository: diningRepository) let uploadFileUseCase = DefaultUploadFileUseCase(coreRepository: coreRepository) let assignAbTestUseCase = DefaultAssignAbTestUseCase(abTestRepository: abTestRepository) @@ -76,7 +76,7 @@ extension HomeHostingController { shareMenuListUseCase: shareMenuListUseCase, fetchDiningNotiUseCase: fetchDiningNotiUseCase, changeNotiUseCase: changeNotiUseCase, - fetchCurrentDiningItemsUseCase: fetchCurrentDiningItemsUseCase, + fetchOperatingDiningPlacesUseCase: fetchOperatingDiningPlacesUseCase, reportSoldoutDiningUseCase: reportSoldoutDiningUseCase, uploadFileUseCase: uploadFileUseCase, assignAbTestUseCase: assignAbTestUseCase, diff --git a/Koin/Presentation/Home/Notification/NotificationViewController.swift b/Koin/Presentation/Home/Notification/NotificationViewController.swift index a54524db..dfa68ed4 100644 --- a/Koin/Presentation/Home/Notification/NotificationViewController.swift +++ b/Koin/Presentation/Home/Notification/NotificationViewController.swift @@ -262,7 +262,7 @@ extension NotificationViewController { let shareMenuListUseCase = DefaultShareMenuListUseCase(diningRepository: diningRepository) let fetchDiningNotiUseCase = DefaultFetchDiningNotiUseCase(notiRepository: notiReposotiry) let changeNotiUseCase = DefaultChangeNotiUseCase(notiRepository: notiReposotiry) - let fetchCurrentDiningItemsUseCase = DefaultFetchCurrentDiningItemsUseCase(repository: diningRepository) + let fetchOperatingDiningPlacesUseCase = DefaultFetchOperatingDiningPlacesUseCase(repository: diningRepository) let reportSoldoutDiningUseCase = DefaultReportSoldoutDiningUseCase(repository: diningRepository) let uploadFileUseCase = DefaultUploadFileUseCase(coreRepository: coreRepository) let assignAbTestUseCase = DefaultAssignAbTestUseCase(abTestRepository: abTestRepository) @@ -273,7 +273,7 @@ extension NotificationViewController { shareMenuListUseCase: shareMenuListUseCase, fetchDiningNotiUseCase: fetchDiningNotiUseCase, changeNotiUseCase: changeNotiUseCase, - fetchCurrentDiningItemsUseCase: fetchCurrentDiningItemsUseCase, + fetchOperatingDiningPlacesUseCase: fetchOperatingDiningPlacesUseCase, reportSoldoutDiningUseCase: reportSoldoutDiningUseCase, uploadFileUseCase: uploadFileUseCase, assignAbTestUseCase: assignAbTestUseCase, diff --git a/koin.xcodeproj/project.pbxproj b/koin.xcodeproj/project.pbxproj index 352fc1ed..824a36b0 100644 --- a/koin.xcodeproj/project.pbxproj +++ b/koin.xcodeproj/project.pbxproj @@ -196,6 +196,7 @@ 7C520869306902C10009BDD3 /* RecruitApplyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C520868306902C10009BDD3 /* RecruitApplyRequest.swift */; }; 7C5583602F11778B00D3A980 /* ShopInfoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C55835F2F11778B00D3A980 /* ShopInfoHeaderView.swift */; }; 7C5583622F11779400D3A980 /* ShopInfoFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5583612F11779400D3A980 /* ShopInfoFooterView.swift */; }; + 7C58FB2E30920D43006B4655 /* DiningReportRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C58FB2D30920D43006B4655 /* DiningReportRequest.swift */; }; 7C58FE862F5F9CDD00A2737B /* CallVanDataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C58FE852F5F9CDD00A2737B /* CallVanDataViewController.swift */; }; 7C58FE892F5F9E7600A2737B /* CallVanData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C58FE882F5F9E7600A2737B /* CallVanData.swift */; }; 7C58FE8E2F5FA4D200A2737B /* CallVanDataTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C58FE8D2F5FA4D200A2737B /* CallVanDataTableView.swift */; }; @@ -778,7 +779,7 @@ 7CFC441B30822E3400623C1F /* DiningReportAbTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFC441A30822E3400623C1F /* DiningReportAbTest.swift */; }; 7CFC441D30837E4900623C1F /* CancelableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFC441C30837E4900623C1F /* CancelableImage.swift */; }; 7CFC44233083DDA000623C1F /* DiningReportModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFC44223083DDA000623C1F /* DiningReportModalView.swift */; }; - 7CFC4425308404E300623C1F /* FetchCurrentDiningItemsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFC4424308404E300623C1F /* FetchCurrentDiningItemsUseCase.swift */; }; + 7CFC4425308404E300623C1F /* FetchOperatingDiningPlacesUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFC4424308404E300623C1F /* FetchOperatingDiningPlacesUseCase.swift */; }; 7CFC44273084111100623C1F /* ReportSoldoutDiningUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFC44263084111100623C1F /* ReportSoldoutDiningUseCase.swift */; }; 7CFC44293084131A00623C1F /* ReportSoldoutRequestDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFC44283084131A00623C1F /* ReportSoldoutRequestDto.swift */; }; 7CFC442B3085217F00623C1F /* DiningNotiBottomSheetContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFC442A3085217F00623C1F /* DiningNotiBottomSheetContentView.swift */; }; @@ -1460,6 +1461,7 @@ 7C520868306902C10009BDD3 /* RecruitApplyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecruitApplyRequest.swift; sourceTree = ""; }; 7C55835F2F11778B00D3A980 /* ShopInfoHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopInfoHeaderView.swift; sourceTree = ""; }; 7C5583612F11779400D3A980 /* ShopInfoFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopInfoFooterView.swift; sourceTree = ""; }; + 7C58FB2D30920D43006B4655 /* DiningReportRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningReportRequest.swift; sourceTree = ""; }; 7C58FE852F5F9CDD00A2737B /* CallVanDataViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanDataViewController.swift; sourceTree = ""; }; 7C58FE882F5F9E7600A2737B /* CallVanData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanData.swift; sourceTree = ""; }; 7C58FE8D2F5FA4D200A2737B /* CallVanDataTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVanDataTableView.swift; sourceTree = ""; }; @@ -2021,7 +2023,7 @@ 7CFC441A30822E3400623C1F /* DiningReportAbTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningReportAbTest.swift; sourceTree = ""; }; 7CFC441C30837E4900623C1F /* CancelableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelableImage.swift; sourceTree = ""; }; 7CFC44223083DDA000623C1F /* DiningReportModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningReportModalView.swift; sourceTree = ""; }; - 7CFC4424308404E300623C1F /* FetchCurrentDiningItemsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCurrentDiningItemsUseCase.swift; sourceTree = ""; }; + 7CFC4424308404E300623C1F /* FetchOperatingDiningPlacesUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchOperatingDiningPlacesUseCase.swift; sourceTree = ""; }; 7CFC44263084111100623C1F /* ReportSoldoutDiningUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSoldoutDiningUseCase.swift; sourceTree = ""; }; 7CFC44283084131A00623C1F /* ReportSoldoutRequestDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSoldoutRequestDto.swift; sourceTree = ""; }; 7CFC442A3085217F00623C1F /* DiningNotiBottomSheetContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiningNotiBottomSheetContentView.swift; sourceTree = ""; }; @@ -5995,6 +5997,7 @@ DA2100000000000000000005 /* CoopOpen.swift */, D2A513402C59FE860064941A /* ShareDiningMenu.swift */, 7CFC441A30822E3400623C1F /* DiningReportAbTest.swift */, + 7C58FB2D30920D43006B4655 /* DiningReportRequest.swift */, ); path = Dining; sourceTree = ""; @@ -6632,7 +6635,7 @@ isa = PBXGroup; children = ( D249CD3C2C159350000813F9 /* DateProvider.swift */, - 7CFC4424308404E300623C1F /* FetchCurrentDiningItemsUseCase.swift */, + 7CFC4424308404E300623C1F /* FetchOperatingDiningPlacesUseCase.swift */, D249CD252C12DC2C000813F9 /* FetchDiningListUseCase.swift */, D2A8F8BF2C4BCD470090C7A4 /* ShareMenuListUseCase.swift */, 7CFC44263084111100623C1F /* ReportSoldoutDiningUseCase.swift */, @@ -6893,6 +6896,7 @@ FE01000000000000000026 /* KoinDropdownConfiguration.swift in Sources */, D208A7CD2CABB7A3007040E7 /* CoreAPI.swift in Sources */, 7CA6867B2FFCB758007937E9 /* DefaultNotificationHistoryRepository.swift in Sources */, + 7C58FB2E30920D43006B4655 /* DiningReportRequest.swift in Sources */, 7CFC441B30822E3400623C1F /* DiningReportAbTest.swift in Sources */, 833D9C052C68F8BA00982145 /* ScreenActionType.swift in Sources */, 835DC2062C84D09400488506 /* FetchRecommendedSearchWordRequest.swift in Sources */, @@ -7776,7 +7780,7 @@ 7C50A1D33041F8CA00897A6F /* RecruitCategory.swift in Sources */, D208A7B92CA1661F007040E7 /* FetchShopBenefitUseCase.swift in Sources */, D20AD6FB2E02A1280067760A /* FindIdEmailResponse.swift in Sources */, - 7CFC4425308404E300623C1F /* FetchCurrentDiningItemsUseCase.swift in Sources */, + 7CFC4425308404E300623C1F /* FetchOperatingDiningPlacesUseCase.swift in Sources */, B68D2CE52EA505AA00F3B479 /* ToastIntent.swift in Sources */, D2FB236E2C0410510098BA2B /* LogAnalyticsService.swift in Sources */, D22C54752C6A2A80000826DA /* PostReviewUseCase.swift in Sources */, @@ -8278,7 +8282,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = koin/koin.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 453; + CURRENT_PROJECT_VERSION = 454; DEVELOPMENT_TEAM = K626UYGR25; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -8298,7 +8302,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.2.2; + MARKETING_VERSION = 5.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koin.stage; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -8314,7 +8318,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = koin/koin.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 453; + CURRENT_PROJECT_VERSION = 454; DEVELOPMENT_TEAM = K626UYGR25; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -8334,7 +8338,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.2.2; + MARKETING_VERSION = 5.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.bcsdlab.koin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES;