diff --git a/WhereAreYou/WhereAreYou/Data/MockAppointmentCreationRepository.swift b/WhereAreYou/WhereAreYou/Data/MockAppointmentCreationRepository.swift new file mode 100644 index 0000000..c2a713d --- /dev/null +++ b/WhereAreYou/WhereAreYou/Data/MockAppointmentCreationRepository.swift @@ -0,0 +1,35 @@ +// +// MockAppointmentCreationRepository.swift +// WhereAreYou +// +// Created by 이상유 on 2026-07-24. +// + +import Foundation + +final class MockAppointmentCreationRepository: AppointmentCreationRepository { + + func createAppointment( + title: String, + date: Date?, + place: Place?, + completion: @escaping (Result) -> Void + ) { + let code = String(UUID().uuidString.prefix(8)) + let appointment = Appointment( + id: UUID().uuidString, + code: code, + name: title, + dateTime: date ?? Date(), + place: place, + participants: [], + routes: [:], + placeVoteStatus: [:] + ) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + completion(.success(appointment)) + } + } + +} diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentCreationRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentCreationRepository.swift new file mode 100644 index 0000000..5126ef7 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentCreationRepository.swift @@ -0,0 +1,19 @@ +// +// AppointmentCreationRepository.swift +// WhereAreYou +// +// Created by 이상유 on 2026-07-24. +// + +import Foundation + +protocol AppointmentCreationRepository { + + func createAppointment( + title: String, + date: Date?, + place: Place?, + completion: @escaping (Result) -> Void + ) + +} diff --git a/WhereAreYou/WhereAreYou/Domain/UseCase/CreateAppointmentUseCase.swift b/WhereAreYou/WhereAreYou/Domain/UseCase/CreateAppointmentUseCase.swift new file mode 100644 index 0000000..c7460a3 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/UseCase/CreateAppointmentUseCase.swift @@ -0,0 +1,32 @@ +// +// CreateAppointmentUseCase.swift +// WhereAreYou +// +// Created by 이상유 on 2026-07-24. +// + +import Foundation + +final class CreateAppointmentUseCase { + + private let repository: AppointmentCreationRepository + + init(repository: AppointmentCreationRepository) { + self.repository = repository + } + + func execute( + title: String, + date: Date?, + place: Place?, + completion: @escaping (Result) -> Void + ) { + repository.createAppointment( + title: title, + date: date, + place: place, + completion: completion + ) + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift new file mode 100644 index 0000000..aff12f5 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift @@ -0,0 +1,190 @@ +// +// AppointmentCreationViewController.swift +// WhereAreYou +// +// Created by 이상유 on 2026-07-24. +// + +import UIKit +import Combine + +final class AppointmentCreationViewController: UIViewController { + + private let viewModel: AppointmentCreationViewModel + private var cancellables = Set() + + private let logoImage: UIImageView = { + let imageView = UIImageView(image: .logo) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private let creationCard = AppointmentCreateCard() + + private var confirmCard: AppointmentConfirmCard? + + init(_ viewModel: AppointmentCreationViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .pointBackground + title = "약속 생성" + setUpLayout() + setUpActions() + bindViewModel() + } + + private func setUpLayout() { + logoImage.translatesAutoresizingMaskIntoConstraints = false + creationCard.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(logoImage) + view.addSubview(creationCard) + + NSLayoutConstraint.activate([ + logoImage.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + logoImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + logoImage.widthAnchor.constraint(equalToConstant: 77), + logoImage.heightAnchor.constraint(equalToConstant: 48), + + creationCard.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor, constant: -50), + creationCard.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 45), + creationCard.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -45), + ]) + } + + private func setUpActions() { + creationCard.onNameChanged = { [weak self] new in + self?.viewModel.setTitle(new) + } + creationCard.onDateRowTap = { [weak self] in + self?.presentDatePicker() + } + creationCard.onPlaceRowTap = { [weak self] in + self?.presentPlaceSearch() + } + creationCard.onMapButtonTap = { + // TODO: 지도 화면 작업 후 연결 + } + creationCard.onCreateTap = { [weak self] in + self?.viewModel.create() + } + } + + private func bindViewModel() { + viewModel.$appointmentDate + .receive(on: DispatchQueue.main) + .sink { [weak self] date in + guard let self, let date else { return } + self.creationCard.setDateLabel(date) + } + .store(in: &cancellables) + + viewModel.$appointmentPlace + .receive(on: DispatchQueue.main) + .sink { [weak self] place in + guard let self, let place else { return } + self.creationCard.setPlaceLabel(place) + } + .store(in: &cancellables) + + viewModel.$hasCreated + .receive(on: DispatchQueue.main) + .sink { [weak self] hasCreated in + guard let self, hasCreated else { return } + self.showConfirmCard() + } + .store(in: &cancellables) + } + +} + + +extension AppointmentCreationViewController { + + // MARK: - Confirm Card Transition + + private func showConfirmCard() { + let info = viewModel.makeAppointmentInfo() + let card = AppointmentConfirmCard(data: info) + self.confirmCard = card + + card.onCopyCodeTap = { + UIPasteboard.general.string = info.code + } + card.onConfirmTap = { + // TODO: 약속 화면으로 이동 + } + + view.addSubview(card) + NSLayoutConstraint.activate([ + card.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor, constant: -50), + card.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 45), + card.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -45), + ]) + + card.transform = CGAffineTransform(translationX: view.bounds.width, y: 0) + card.alpha = 0 + + UIView.animate(withDuration: 0.45, delay: 0, usingSpringWithDamping: 0.85, initialSpringVelocity: 0.5) { + self.creationCard.transform = CGAffineTransform(translationX: -self.view.bounds.width, y: 0) + self.creationCard.alpha = 0 + card.transform = .identity + card.alpha = 1 + } + } + + // MARK: - Date Picker + + private func presentDatePicker() { + let pickerViewController = DatePickerSheetViewController( + title: "약속 날짜/시간 설정", + initialDate: viewModel.appointmentDate + ) + pickerViewController.onDateSelected = { [weak self] date in + self?.viewModel.setDate(date) + } + + if let sheet = pickerViewController.sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = true + } + + present(pickerViewController, animated: true) + } + + + // MARK: - Search Place Card + + private func presentPlaceSearch() { + let searchPlaceViewController = SearchPlaceCardViewController( + viewModel: SearchPlaceCardViewModel( + searchPlacesUseCase: SearchPlacesUseCase( + repository: MockPlaceSearchRepository() + ) + ), + selectionButtonTitle: "선택하기", + style: .onlyHeader(title: "약속 장소 검색") + ) + + searchPlaceViewController.onPlaceSelected = { [weak self] place in + self?.viewModel.setPlace(place) + searchPlaceViewController.dismiss(animated: true) + } + + if let sheet = searchPlaceViewController.sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = true + } + + present(searchPlaceViewController, animated: true) + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift new file mode 100644 index 0000000..ffe6983 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift @@ -0,0 +1,84 @@ +// +// AppointmentCreationViewModel.swift +// WhereAreYou +// +// Created by 이상유 on 2026-07-24. +// + +import Foundation +import Combine + +final class AppointmentCreationViewModel { + + private let createAppointmentUseCase: CreateAppointmentUseCase + + private var appointmentTitle: String + private var id: String? + private var code: String? + private var isCreating = false + + @Published private(set) var appointmentDate: Date? + @Published private(set) var appointmentPlace: Place? + @Published private(set) var hasCreated: Bool = false + + init(createAppointmentUseCase: CreateAppointmentUseCase) { + self.createAppointmentUseCase = createAppointmentUseCase + appointmentTitle = "약속" + } + + func setTitle(_ title: String) { + appointmentTitle = title + } + + func setDate(_ date: Date) { + appointmentDate = date + } + + func setPlace(_ place: Place) { + appointmentPlace = place + } + + func create() { + guard !isCreating, !hasCreated else { return } + isCreating = true + if appointmentTitle.isEmpty { appointmentTitle = "약속" } + + createAppointmentUseCase.execute( + title: appointmentTitle, + date: appointmentDate, + place: appointmentPlace + ) { [weak self] result in + guard let self else { return } + DispatchQueue.main.async { + self.isCreating = false + switch result { + case .success(let appointment): + self.id = appointment.id + self.code = appointment.code + self.hasCreated = true + case .failure: + // TODO: 서버 작업 후 실패 케이스 정리해 사용자에게 표시할 실패 텍스트 추가 예정 + break + } + } + } + } + + func makeAppointmentInfo() -> AppointmentInfo { + AppointmentInfo( + id: id ?? "", + code: code ?? "", + title: appointmentTitle, + date: appointmentDate, + location: appointmentPlace.map { + AppointmentLocation( + title: $0.name, + address: $0.address, + coordinate: $0.coordinate + ) + }, + participants: [] + ) + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentConfirmCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentConfirmCard.swift similarity index 87% rename from WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentConfirmCard.swift rename to WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentConfirmCard.swift index abcd446..d9c2693 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentConfirmCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentConfirmCard.swift @@ -34,8 +34,8 @@ final class AppointmentConfirmCard: UIView { return label }() - private lazy var copyButton = UIButton.filled(title: "코드 복사하기", background: .systemGray2, tint: .white) - private lazy var confirmButton = UIButton.filled(title: "약속으로 이동하기", background: .blue2, tint: .white) + private lazy var copyButton = UIButton.filled(title: "코드 복사하기", background: .systemGray2, tint: .white, font: .subheadline) + private lazy var confirmButton = UIButton.filled(title: "약속으로 이동하기", background: .blue2, tint: .white, font: .subheadline) private let footerStack: UIStackView = { let stack = UIStackView() @@ -78,8 +78,8 @@ final class AppointmentConfirmCard: UIView { card.contentStack.addArrangedSubview(codeLabel) card.contentStack.addArrangedSubview(footerStack) - copyButton.addTarget(self, action: #selector(copyTapped), for: .touchUpInside) - confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) + copyButton.addAction(UIAction { [weak self] _ in self?.onCopyCodeTap?() }, for: .touchUpInside) + confirmButton.addAction(UIAction { [weak self] _ in self?.onConfirmTap?() }, for: .touchUpInside) } private func configure(with data: AppointmentInfo) { @@ -89,7 +89,4 @@ final class AppointmentConfirmCard: UIView { codeLabel.text = "약속 코드 : \(data.code)" } - @objc private func copyTapped() { onCopyCodeTap?() } - @objc private func confirmTapped() { onConfirmTap?() } - } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreateCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentCreateCard.swift similarity index 88% rename from WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreateCard.swift rename to WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentCreateCard.swift index 41ca6b1..f7b305a 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreateCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentCreateCard.swift @@ -21,7 +21,6 @@ final class AppointmentCreateCard: UIView { init() { super.init(frame: .zero) - fieldsBox.name = "약속" setUp() } @@ -29,6 +28,14 @@ final class AppointmentCreateCard: UIView { fatalError("init(coder:) has not been implemented — use init()") } + func setDateLabel(_ date: Date) { + fieldsBox.dateText = date.appointmentDateTimeText + } + + func setPlaceLabel(_ place: Place) { + fieldsBox.placeText = place.name + } + private func setUp() { translatesAutoresizingMaskIntoConstraints = false card.translatesAutoresizingMaskIntoConstraints = false @@ -48,7 +55,7 @@ final class AppointmentCreateCard: UIView { fieldsBox.onDateRowTap = { [weak self] in self?.onDateRowTap?() } fieldsBox.onPlaceRowTap = { [weak self] in self?.onPlaceRowTap?() } fieldsBox.onMapButtonTap = { [weak self] in self?.onMapButtonTap?() } - createButton.addTarget(self, action: #selector(createTapped), for: .touchUpInside) + createButton.addAction(UIAction { [weak self] _ in self?.onCreateTap?() }, for: .touchUpInside) let dismissKeyboardGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) dismissKeyboardGesture.cancelsTouchesInView = false @@ -56,7 +63,6 @@ final class AppointmentCreateCard: UIView { addGestureRecognizer(dismissKeyboardGesture) } - @objc private func createTapped() { onCreateTap?() } @objc private func dismissKeyboard() { endEditing(true) } } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/InfoRow.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/InfoRow.swift similarity index 100% rename from WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/InfoRow.swift rename to WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/InfoRow.swift diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfoCard.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfoCard.swift index 5f34a57..e22c630 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfoCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfoCard.swift @@ -102,8 +102,8 @@ final class AppointmentInfoCard: UIView { fieldsBox.onDateRowTap = { [weak self] in self?.onDateRowTap?() } fieldsBox.onPlaceRowTap = { [weak self] in self?.onPlaceRowTap?() } fieldsBox.onMapButtonTap = { [weak self] in self?.onMapButtonTap?() } - leaveButton.addTarget(self, action: #selector(leaveTapped), for: .touchUpInside) - copyButton.addTarget(self, action: #selector(copyTapped), for: .touchUpInside) + leaveButton.addAction(UIAction { [weak self] _ in self?.onLeaveTap?() }, for: .touchUpInside) + copyButton.addAction(UIAction { [weak self] _ in self?.onCopyCodeTap?() }, for: .touchUpInside) let dismissKeyboardGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) dismissKeyboardGesture.cancelsTouchesInView = false @@ -124,8 +124,6 @@ final class AppointmentInfoCard: UIView { codeLabel.text = "약속 코드 : \(data.code)" } - @objc private func leaveTapped() { onLeaveTap?() } - @objc private func copyTapped() { onCopyCodeTap?() } @objc private func dismissKeyboard() { endEditing(true) } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/AppointmentFieldsBox.swift b/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/AppointmentFieldsBox.swift index 0f8b95d..3dfd4cc 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/AppointmentFieldsBox.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/AppointmentFieldsBox.swift @@ -27,7 +27,7 @@ final class AppointmentFieldsBox: UIView { set { placeRow.text = newValue } } - private let nameField = TextFieldRow(icon: UIImage(systemName: "tag"), placeholder: "약속") + private let nameField = TextFieldRow(icon: UIImage(systemName: "tag"), placeholder: "약속 이름을 정해주세요.") private let dateRow = ButtonRow(icon: UIImage(systemName: "calendar"), placeholder: "날짜와 시간을 선택해주세요.") private let placeRow = ButtonRow(icon: UIImage(systemName: "location.circle"), placeholder: "장소를 선택해주세요.") @@ -51,7 +51,7 @@ final class AppointmentFieldsBox: UIView { } private func setUp() { - mapButton.addTarget(self, action: #selector(mapTapped), for: .touchUpInside) + mapButton.addAction(UIAction { [weak self] _ in self?.onMapButtonTap?() }, for: .touchUpInside) mapButton.widthAnchor.constraint(equalToConstant: 45).isActive = true let placeRowContent = UIStackView(arrangedSubviews: [placeRow, mapButton]) @@ -91,6 +91,5 @@ final class AppointmentFieldsBox: UIView { return stack } - @objc private func mapTapped() { onMapButtonTap?() } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/ButtonRow.swift b/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/ButtonRow.swift index 9207180..3947d57 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/ButtonRow.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/ButtonRow.swift @@ -68,11 +68,9 @@ final class ButtonRow: UIControl { stack.centerYAnchor.constraint(equalTo: centerYAnchor) ]) - addTarget(self, action: #selector(tapped), for: .touchUpInside) + addAction(UIAction { [weak self] _ in self?.onTap?() }, for: .touchUpInside) } - @objc private func tapped() { onTap?() } - private func updateHighlightAppearance() { UIView.animate(withDuration: 0.12) { self.backgroundColor = self.isHighlighted ? UIColor.systemGray6 : .clear diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/TextFieldRow.swift b/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/TextFieldRow.swift index 9df1fc1..caa2cff 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/TextFieldRow.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/AppointmentFieldsBox/TextFieldRow.swift @@ -57,9 +57,9 @@ final class TextFieldRow: UIView { stack.centerYAnchor.constraint(equalTo: centerYAnchor) ]) - textField.addTarget(self, action: #selector(textChanged), for: .editingChanged) + textField.addAction(UIAction { [weak self] _ in + self?.onTextChanged?(self?.textField.text ?? "") + }, for: .editingChanged) } - - @objc private func textChanged() { onTextChanged?(textField.text ?? "") } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/CardContainerView.swift b/WhereAreYou/WhereAreYou/Presentation/Component/CardContainerView.swift index 42edc74..0ac3e69 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/CardContainerView.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/CardContainerView.swift @@ -118,10 +118,8 @@ final class CardContainerView: UIView { let button = UIButton(type: .close) button.setImage(UIImage(systemName: "xmark"), for: .normal) button.tintColor = .label - button.addTarget(self, action: #selector(closeTapped), for: .touchUpInside) + button.addAction(UIAction { [weak self] _ in self?.onClose?() }, for: .touchUpInside) return button } - @objc private func closeTapped() { onClose?() } - } diff --git a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/DepartureTimePickerViewController.swift b/WhereAreYou/WhereAreYou/Presentation/Component/DatePickerSheetViewController.swift similarity index 88% rename from WhereAreYou/WhereAreYou/Presentation/RouteSearch/DepartureTimePickerViewController.swift rename to WhereAreYou/WhereAreYou/Presentation/Component/DatePickerSheetViewController.swift index c4143d5..2477b55 100644 --- a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/DepartureTimePickerViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/DatePickerSheetViewController.swift @@ -1,5 +1,5 @@ // -// DepartureTimePickerViewController.swift +// DatePickerSheetViewController.swift // WhereAreYou // // Created by 이상유 on 2026-07-22. @@ -7,13 +7,12 @@ import UIKit -final class DepartureTimePickerViewController: UIViewController { +final class DatePickerSheetViewController: UIViewController { var onDateSelected: ((Date) -> Void)? private let headerLabel: UILabel = { let label = UILabel() - label.text = "출발 시간 설정" label.font = .preferredFont(forTextStyle: .headline) label.textAlignment = .center return label @@ -35,9 +34,11 @@ final class DepartureTimePickerViewController: UIViewController { edgeInsets: NSDirectionalEdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0) ) - private let initialDate: Date + private let viewTitle: String + private let initialDate: Date? - init(initialDate: Date) { + init(title: String, initialDate: Date? = nil) { + self.viewTitle = title self.initialDate = initialDate super.init(nibName: nil, bundle: nil) } @@ -50,7 +51,9 @@ final class DepartureTimePickerViewController: UIViewController { super.viewDidLoad() view.backgroundColor = .systemBackground datePicker.minimumDate = Date() - datePicker.date = initialDate + datePicker.date = initialDate ?? Date() + + headerLabel.text = viewTitle headerLabel.translatesAutoresizingMaskIntoConstraints = false datePicker.translatesAutoresizingMaskIntoConstraints = false diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCard.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCard.swift index ee313d3..1369642 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCard.swift @@ -102,8 +102,11 @@ final class SearchPlaceCard: UIView { resultScrollView.addSubview(resultStack) resultScrollView.addSubview(emptyResultLabel) + let expandHeight = resultScrollView.heightAnchor.constraint(equalToConstant: 300) + expandHeight.priority = .defaultLow + NSLayoutConstraint.activate([ - resultScrollView.heightAnchor.constraint(equalToConstant: 300), + expandHeight, resultStack.topAnchor.constraint(equalTo: resultScrollView.contentLayoutGuide.topAnchor), resultStack.bottomAnchor.constraint(equalTo: resultScrollView.contentLayoutGuide.bottomAnchor), @@ -152,6 +155,14 @@ final class SearchPlaceCard: UIView { filterSection.configure(selected: selected) } + func setSearching(_ isSearching: Bool) { + if isSearching { + emptyResultLabel.isHidden = true + } else if places.isEmpty { + emptyResultLabel.isHidden = false + } + } + func updateEmptyResultMessage(hasSearched: Bool) { emptyResultLabel.text = hasSearched ? "검색 결과가 존재하지 않습니다." : "장소를 검색해주세요." } diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewController.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewController.swift index eee23b2..850cc99 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewController.swift @@ -11,8 +11,12 @@ import Combine final class SearchPlaceCardViewController: UIViewController { enum Style { + /// plain: 제목과 dimmed가 없는 카드 형태 case plain + /// dimmed: 제목과 dimmed가 있는 카드 형태 case dimmed(title: String) + /// onlyHeader: dimmed는 없고, 제목이 있는 카드 형태 + case onlyHeader(title: String) } var onPlaceSelected: ((Place) -> Void)? @@ -31,7 +35,7 @@ final class SearchPlaceCardViewController: UIViewController { switch style { case .plain: self.searchCard = SearchPlaceCard(selectionButtonTitle: selectionButtonTitle) - case .dimmed(let title): + case .dimmed(let title), .onlyHeader(let title): self.searchCard = SearchPlaceCard( selectionButtonTitle: selectionButtonTitle, headerStyle: .titleWithCloseButton(title) @@ -86,7 +90,7 @@ final class SearchPlaceCardViewController: UIViewController { searchCard.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor, multiplier: 0.7), ]) - case .plain: + case .plain, .onlyHeader: NSLayoutConstraint.activate([ searchCard.topAnchor.constraint(equalTo: view.topAnchor), searchCard.leadingAnchor.constraint(equalTo: view.leadingAnchor), @@ -149,6 +153,13 @@ final class SearchPlaceCardViewController: UIViewController { } .store(in: &cancellables) + viewModel.$isSearching + .receive(on: DispatchQueue.main) + .sink { [weak self] isSearching in + self?.searchCard.setSearching(isSearching) + } + .store(in: &cancellables) + viewModel.$hasSearched .receive(on: DispatchQueue.main) .sink { [weak self] hasSearched in diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewModel.swift index 55ffeea..dc2b43a 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewModel.swift @@ -12,6 +12,7 @@ final class SearchPlaceCardViewModel { @Published private(set) var filteredPlaces: [Place] = [] @Published private(set) var selectedFilters: [PlaceType] = [] + @Published private(set) var isSearching = false @Published private(set) var hasSearched = false private var allPlaces: [Place] = [] @@ -22,10 +23,12 @@ final class SearchPlaceCardViewModel { } func search(keyword: String) { - hasSearched = true + isSearching = true searchPlacesUseCase.execute(keyword: keyword) { [weak self] result in guard let self else { return } DispatchQueue.main.async { + self.isSearching = false + self.hasSearched = true if case .success(let places) = result { self.allPlaces = places self.applyFilter() diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/FilterTagBox.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/FilterTagBox.swift index 30cb773..f658578 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/FilterTagBox.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/FilterTagBox.swift @@ -110,17 +110,12 @@ final class FilterTagBox: UIView { private func setUpActions() { filters.values.forEach { capsule in - capsule.addTarget(self, action: #selector(filterTapped(_:)), for: .touchUpInside) + capsule.addAction(UIAction { [weak self, weak capsule] _ in + guard let placeType = capsule?.placeType else { return } + self?.onFilterTap?(placeType) + }, for: .touchUpInside) } - resetButton.addTarget(self, action: #selector(resetTapped), for: .touchUpInside) - } - - @objc private func filterTapped(_ sender: FilterCapsule) { - onFilterTap?(sender.placeType) - } - - @objc private func resetTapped() { - onResetTap?() + resetButton.addAction(UIAction { [weak self] _ in self?.onResetTap?() }, for: .touchUpInside) } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceCell.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceCell.swift index 90ba783..1b4c1c2 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceCell.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceCell.swift @@ -79,9 +79,7 @@ final class PlaceCell: UIView { // MARK: - Actions private func setUpActions() { - selectionButton.addTarget(self, action: #selector(selectionButtonTapped), for: .touchUpInside) + selectionButton.addAction(UIAction { [weak self] _ in self?.onButtonTap?() }, for: .touchUpInside) } - @objc private func selectionButtonTapped() { onButtonTap?() } - } diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/SearchBar.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/SearchBar.swift index 65826d8..4d3910a 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/SearchBar.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/SearchBar.swift @@ -65,7 +65,7 @@ final class SearchBar: UIView { searchButton.heightAnchor.constraint(equalToConstant: 22).isActive = true NSLayoutConstraint.activate([ - stack.heightAnchor.constraint(greaterThanOrEqualToConstant: 38), + stack.heightAnchor.constraint(equalToConstant: 38), stack.leadingAnchor.constraint(equalTo: leadingAnchor), stack.trailingAnchor.constraint(equalTo: trailingAnchor), stack.topAnchor.constraint(equalTo: topAnchor), @@ -76,14 +76,13 @@ final class SearchBar: UIView { // MARK: - Actions private func setUpActions() { - textField.addTarget(self, action: #selector(textChanged), for: .editingChanged) - searchButton.addTarget(self, action: #selector(searchTapped), for: .touchUpInside) - } - - @objc private func textChanged() { onTextChanged?(textField.text ?? "" ) } - @objc private func searchTapped() { - onSearchTap?() - endEditing(true) + textField.addAction(UIAction { [weak self] _ in + self?.onTextChanged?(self?.textField.text ?? "") + }, for: .editingChanged) + searchButton.addAction(UIAction { [weak self] _ in + self?.onSearchTap?() + self?.endEditing(true) + }, for: .touchUpInside) } } diff --git a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewController.swift b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewController.swift index e06ae1d..c0b796d 100644 --- a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewController.swift @@ -409,7 +409,10 @@ final class RouteSearchViewController: UIViewController { // MARK: - Time picker private func presentTimePicker() { - let pickerViewController = DepartureTimePickerViewController(initialDate: viewModel.departureTime) + let pickerViewController = DatePickerSheetViewController( + title: "출발 시간 설정", + initialDate: viewModel.departureTime + ) pickerViewController.onDateSelected = { [weak self] date in self?.viewModel.setDepartureTime(date) } diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Contents.json b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Contents.json index db65b93..1f8162a 100644 --- a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Contents.json +++ b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "Logo@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "Logo.png", + "idiom" : "universal" } ], "info" : { diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Logo.png b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Logo.png new file mode 100644 index 0000000..a281510 Binary files /dev/null and b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Logo.png differ diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Logo@2x.png b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Logo@2x.png deleted file mode 100644 index 768d945..0000000 Binary files a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Logo.imageset/Logo@2x.png and /dev/null differ