diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift index 7f0e1f0..51c9585 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift @@ -87,11 +87,11 @@ final class AppointmentCreationViewController: UIViewController { } .store(in: &cancellables) - viewModel.$appointmentPlace + viewModel.$place .receive(on: DispatchQueue.main) - .sink { [weak self] place in - guard let self, let place else { return } - self.creationCard.setPlaceLabel(place) + .sink { [weak self] placeInfo in + guard let self, let placeInfo else { return } + self.creationCard.setPlaceLabel(name: placeInfo.name) } .store(in: &cancellables) @@ -198,7 +198,7 @@ extension AppointmentCreationViewController { nearbyPlaceRepository: MockNearbyPlaceRepository(), reverseGeocodingRepository: MockReverseGeocodingRepository() ), - initialCoordinate: viewModel.appointmentPlace?.coordinate + initialCoordinate: viewModel.place?.coordinate ) let placeSelectionViewController = PlaceSelectionViewController(viewModel: placeSelectionViewModel) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift index ffe6983..b03b5ef 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift @@ -16,9 +16,10 @@ final class AppointmentCreationViewModel { private var id: String? private var code: String? private var isCreating = false + private var placeDomain: Place? @Published private(set) var appointmentDate: Date? - @Published private(set) var appointmentPlace: Place? + @Published private(set) var place: PlaceInfo? @Published private(set) var hasCreated: Bool = false init(createAppointmentUseCase: CreateAppointmentUseCase) { @@ -35,7 +36,8 @@ final class AppointmentCreationViewModel { } func setPlace(_ place: Place) { - appointmentPlace = place + placeDomain = place + self.place = PlaceInfo(place: place) } func create() { @@ -46,7 +48,7 @@ final class AppointmentCreationViewModel { createAppointmentUseCase.execute( title: appointmentTitle, date: appointmentDate, - place: appointmentPlace + place: placeDomain ) { [weak self] result in guard let self else { return } DispatchQueue.main.async { @@ -70,13 +72,7 @@ final class AppointmentCreationViewModel { code: code ?? "", title: appointmentTitle, date: appointmentDate, - location: appointmentPlace.map { - AppointmentLocation( - title: $0.name, - address: $0.address, - coordinate: $0.coordinate - ) - }, + location: placeDomain.map { AppointmentLocation(place: $0) }, participants: [] ) } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentCreateCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentCreateCard.swift index f7b305a..0fcfb47 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentCreateCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentCreateCard.swift @@ -32,8 +32,8 @@ final class AppointmentCreateCard: UIView { fieldsBox.dateText = date.appointmentDateTimeText } - func setPlaceLabel(_ place: Place) { - fieldsBox.placeText = place.name + func setPlaceLabel(name: String) { + fieldsBox.placeText = name } private func setUp() { diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoViewModel.swift index 5fac7a7..be8d2e1 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoViewModel.swift @@ -30,7 +30,7 @@ final class AppointmentInfoViewModel { fetchAppointmentInfoUseCase.execute(appointmentID: appointmentID) { [weak self] result in DispatchQueue.main.async { if case .success(let appointment) = result { - self?.appointmentInfo = self?.buildAppointmentInfo(from: appointment) + self?.appointmentInfo = AppointmentInfo(appointment: appointment) } } } @@ -58,27 +58,10 @@ final class AppointmentInfoViewModel { ) { [weak self] result in DispatchQueue.main.async { if case .success(let appointment) = result { - self?.appointmentInfo = self?.buildAppointmentInfo(from: appointment) + self?.appointmentInfo = AppointmentInfo(appointment: appointment) } } } } - private func buildAppointmentInfo(from appointment: Appointment) -> AppointmentInfo { - let location = appointment.place.map { - AppointmentLocation(title: $0.name, address: $0.address, coordinate: $0.coordinate) - } - let participants = appointment.participants.map { - Participant(id: $0.id, nickname: $0.nickname, profileImage: $0.profileImage.lastPathComponent) - } - return AppointmentInfo( - id: appointment.id, - code: appointment.code, - title: appointment.name, - date: appointment.dateTime, - location: location, - participants: participants - ) - } - } diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift index 3367f4f..afffa10 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift @@ -157,7 +157,7 @@ private extension ChatViewModel { id: message.id, senderID: message.sender.id, senderNickname: message.sender.nickname, - senderProfileImage: message.sender.profileImage.lastPathComponent, + senderProfileImage: message.sender.profileImageName, content: bubbleContent, timeText: timeText, sentAt: message.sentAt, diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/SharedPlace/SharedPlacesViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/SharedPlace/SharedPlacesViewModel.swift index f175deb..175ce96 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Chat/SharedPlace/SharedPlacesViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Chat/SharedPlace/SharedPlacesViewModel.swift @@ -70,15 +70,8 @@ final class SharedPlacesViewModel { } private func buildItems() { - var result = sharedPlaces.map { shared in - SharedPlaceItem( - id: shared.id, - placeName: shared.place.name, - placeAddress: shared.place.address, - voterProfileImages: shared.voters.map { $0.profileImage.lastPathComponent }, - hasVoted: shared.voters.contains { $0.id == currentUserID }, - sharedAt: shared.sharedAt - ) + var result = sharedPlaces.map { + SharedPlaceItem(sharedPlace: $0, currentUserID: currentUserID) } switch sortOrder { diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewController.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewController.swift index 6d6b832..105e937 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewController.swift @@ -184,10 +184,7 @@ final class SearchPlaceCardViewController: UIViewController { viewModel.$filteredPlaces .receive(on: DispatchQueue.main) .sink { [weak self] places in - let infos = places.map { - PlaceInfo(id: $0.id, name: $0.name, address: $0.address, tag: $0.type) - } - self?.searchCard.updatePlaces(infos) + self?.searchCard.updatePlaces(places) } .store(in: &cancellables) diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewModel.swift index 5aa129c..4472a95 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SearchPlaceCardViewModel.swift @@ -10,7 +10,7 @@ import Combine final class SearchPlaceCardViewModel { - @Published private(set) var filteredPlaces: [Place] = [] + @Published private(set) var filteredPlaces: [PlaceInfo] = [] @Published private(set) var selectedFilters: [PlaceType] = [] @Published private(set) var isSearching = false @Published private(set) var hasSearched = false @@ -63,11 +63,13 @@ final class SearchPlaceCardViewModel { } private func applyFilter() { + let filtered: [Place] if selectedFilters.isEmpty { - filteredPlaces = allPlaces + filtered = allPlaces } else { - filteredPlaces = allPlaces.filter { selectedFilters.contains($0.type) } + filtered = allPlaces.filter { selectedFilters.contains($0.type) } } + filteredPlaces = filtered.map { PlaceInfo(place: $0) } } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/FilterTagBox.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/FilterTagBox.swift index f658578..d792f00 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/FilterTagBox.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/FilterTagBox.swift @@ -140,7 +140,7 @@ extension FilterTagBox { init(placeType: PlaceType) { self.placeType = placeType - self.capsule = PlaceTagCapsule(placeType) + self.capsule = PlaceTagCapsule(title: placeType.title, color: placeType.color.uiColor) super.init(frame: .zero) setUp() } @@ -168,7 +168,7 @@ extension FilterTagBox { private func updateAppearance() { let baseColor: UIColor = isSelected - ? placeType.color + ? placeType.color.uiColor : UIColor.systemGray2.withAlphaComponent(0.3) capsule.setColor(isHighlighted ? baseColor.withAlphaComponent(0.6) : baseColor) } diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceCell.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceCell.swift index 1b4c1c2..939c4ae 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceCell.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceCell.swift @@ -29,7 +29,7 @@ final class PlaceCell: UIView { private let selectionButton: UIButton init(_ place: PlaceInfo, buttonText: String) { - self.placeTag = PlaceTagCapsule(place.tag) + self.placeTag = PlaceTagCapsule(title: place.tagTitle, color: place.tagColor.uiColor) self.selectionButton = UIButton.filled( title: buttonText, background: .blue2.withAlphaComponent(0.85), diff --git a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceTagCapsule.swift b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceTagCapsule.swift index 0b3ea71..9092d42 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceTagCapsule.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Component/SearchPlaceCard/SubComponent/PlaceTagCapsule.swift @@ -16,10 +16,10 @@ final class PlaceTagCapsule: UIView { return label }() - init(_ placeType: PlaceType) { + init(title: String, color: UIColor) { super.init(frame: .zero) - label.text = placeType.title - setUp(backgroundColor: placeType.color) + label.text = title + setUp(backgroundColor: color) } required init?(coder: NSCoder) { diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/ColorAsset+UIColor.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/ColorAsset+UIColor.swift new file mode 100644 index 0000000..8ef88e4 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/Extension/ColorAsset+UIColor.swift @@ -0,0 +1,24 @@ +// +// ColorAsset+UIColor.swift +// WhereAreYou +// +// Created by 이상유 on 2026-08-18. +// + +import UIKit + +extension ColorAsset { + + var uiColor: UIColor { + switch self { + case .green: return .systemGreen + case .yellow: return .systemYellow + case .orange: return .orange + case .indigo: return .systemIndigo + case .blue: return .systemBlue + case .red: return .systemRed + case .brown: return .systemBrown + } + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/PlaceType+.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/PlaceType+.swift index 2fc9d34..3c3c0b8 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Extension/PlaceType+.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Extension/PlaceType+.swift @@ -5,7 +5,7 @@ // Created by 이상유 on 2026-07-16. // -import UIKit +import Foundation extension PlaceType { @@ -21,15 +21,15 @@ extension PlaceType { } } - var color: UIColor { + var color: ColorAsset { switch self { - case .subway: return .systemGreen - case .restaurant: return .systemYellow + case .subway: return .green + case .restaurant: return .yellow case .cafe: return .orange - case .hospital: return .systemIndigo - case .station: return .systemBlue - case .shop: return .systemRed - case .other: return .systemBrown + case .hospital: return .indigo + case .station: return .blue + case .shop: return .red + case .other: return .brown } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/TransportType+.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/TransportType+.swift index 99b7232..2764238 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Extension/TransportType+.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Extension/TransportType+.swift @@ -5,7 +5,7 @@ // Created by 이상유 on 2026-07-14. // -import UIKit +import Foundation extension TransportType { @@ -25,11 +25,11 @@ extension TransportType { } } - var color: UIColor { + var color: ColorAsset { switch self { - case .walk: return .systemGreen - case .car: return .systemIndigo - case .transit: return .systemYellow + case .walk: return .green + case .car: return .indigo + case .transit: return .yellow } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/User+ProfileImage.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/User+ProfileImage.swift new file mode 100644 index 0000000..3ea6cae --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/Extension/User+ProfileImage.swift @@ -0,0 +1,18 @@ +// +// User+ProfileImage.swift +// WhereAreYou +// +// Created by 이상유 on 2026-08-17. +// + +import Foundation + +extension User { + + /// 프로필 이미지 URL에서 화면에 사용할 에셋 이름만 추출 + /// - 서버 연동 후 이미지 표현이 바뀌면 이 프로퍼티 한 곳만 수정하면 된다. + var profileImageName: String { + profileImage.lastPathComponent + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteInfoView.swift b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteInfoView.swift index b19f728..1bcb451 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteInfoView.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteInfoView.swift @@ -62,7 +62,7 @@ final class AppointmentRouteInfoView: UIView { routeSummaryCard.setRemaining(timeText: timeText) } - func setSteps(_ steps: [RouteStep]) { + func setSteps(_ steps: [RouteStepItem]) { routeSummaryCard.setSteps(steps) } @@ -225,7 +225,7 @@ private final class RouteSummaryCardView: UIView { remainingTimeSummary.valueLabel.text = timeText } - func setSteps(_ steps: [RouteStep]) { + func setSteps(_ steps: [RouteStepItem]) { stepLabelsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } stepBarsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } stepBarWidthConstraints.forEach { $0.isActive = false } @@ -236,18 +236,18 @@ private final class RouteSummaryCardView: UIView { guard !steps.isEmpty else { return } - let totalTime = max(steps.reduce(0) { $0 + $1.estimatedTime }, 1) + let totalTime = max(steps.reduce(0) { $0 + $1.estimatedTimeMinutes }, 1) for step in steps { - let icon = UIImageView(image: UIImage(systemName: step.transportType.icon)) - icon.tintColor = step.transportType.color + let icon = UIImageView(image: UIImage(systemName: step.transportIcon)) + icon.tintColor = step.transportColor.uiColor icon.contentMode = .scaleAspectFit icon.translatesAutoresizingMaskIntoConstraints = false icon.widthAnchor.constraint(equalToConstant: 14).isActive = true icon.heightAnchor.constraint(equalToConstant: 14).isActive = true let label = UILabel() - label.text = "\(Int(step.estimatedTime))분" + label.text = "\(Int(step.estimatedTimeMinutes))분" label.font = .preferredFont(forTextStyle: .caption2) label.textColor = .secondaryLabel @@ -258,7 +258,7 @@ private final class RouteSummaryCardView: UIView { stepLabelsStack.addArrangedSubview(labelStack) let bar = UIView() - bar.backgroundColor = step.transportType.color + bar.backgroundColor = step.transportColor.uiColor bar.layer.cornerRadius = 2 bar.heightAnchor.constraint(equalToConstant: 4).isActive = true stepBarsStack.addArrangedSubview(bar) @@ -271,7 +271,7 @@ private final class RouteSummaryCardView: UIView { let minWidth = labelStack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width stepMinWidths.append(minWidth) - stepRatios.append(CGFloat(step.estimatedTime / totalTime)) + stepRatios.append(CGFloat(step.estimatedTimeMinutes / totalTime)) } setNeedsLayout() diff --git a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/ParticipantRouteRow.swift b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/ParticipantRouteRow.swift index a138dd0..ff767b1 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/ParticipantRouteRow.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/ParticipantRouteRow.swift @@ -58,17 +58,17 @@ final class ParticipantRouteRow: UIView { nameColumn.spacing = 2 nameColumn.alignment = .leading - let transportIcon = UIImageView(image: UIImage(systemName: participant.transportType.icon)) - transportIcon.tintColor = participant.transportType.color + let transportIcon = UIImageView(image: UIImage(systemName: participant.transportIcon)) + transportIcon.tintColor = participant.transportColor.uiColor transportIcon.contentMode = .scaleAspectFit transportIcon.translatesAutoresizingMaskIntoConstraints = false transportIcon.widthAnchor.constraint(equalToConstant: 16).isActive = true transportIcon.heightAnchor.constraint(equalToConstant: 16).isActive = true let transportLabel = UILabel() - transportLabel.text = participant.transportType.name + transportLabel.text = participant.transportName transportLabel.font = .preferredFont(forTextStyle: .caption1) - transportLabel.textColor = participant.transportType.color + transportLabel.textColor = participant.transportColor.uiColor let transportStack = UIStackView(arrangedSubviews: [transportIcon, transportLabel]) transportStack.axis = .horizontal diff --git a/WhereAreYou/WhereAreYou/Presentation/Map/PlaceSelection/PlaceSelectionViewController.swift b/WhereAreYou/WhereAreYou/Presentation/Map/PlaceSelection/PlaceSelectionViewController.swift index f1db18b..0fcc8f4 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Map/PlaceSelection/PlaceSelectionViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Map/PlaceSelection/PlaceSelectionViewController.swift @@ -235,7 +235,7 @@ final class PlaceSelectionViewController: UIViewController { } private func confirmTapped() { - guard let place = viewModel.nearbyPlace else { return } + guard let place = viewModel.nearbyPlaceDomain else { return } onPlaceConfirmed?(place) presentingViewController?.dismiss(animated: true) } diff --git a/WhereAreYou/WhereAreYou/Presentation/Map/PlaceSelection/PlaceSelectionViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Map/PlaceSelection/PlaceSelectionViewModel.swift index d501bc5..d57dea8 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Map/PlaceSelection/PlaceSelectionViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Map/PlaceSelection/PlaceSelectionViewModel.swift @@ -15,10 +15,11 @@ final class PlaceSelectionViewModel { /// 지도 중앙 좌표 기준 근처 장소를 조회 중인지 여부 @Published private(set) var isFetchingNearbyPlace = false /// 지도 중앙 좌표 기준으로 조회된 가장 가까운 장소 1개 - @Published private(set) var nearbyPlace: Place? + @Published private(set) var nearbyPlace: PlaceInfo? /// currentLocation과 nearbyPlace 사이 거리를 표시용 문자열로 계산해둔 값 @Published private(set) var distanceText: String? + private(set) var nearbyPlaceDomain: Place? /// 지도 중앙 좌표가 바뀔 때마다 이벤트를 흘려보내는 파이프 private let centerCoordinateSubject = PassthroughSubject() /// Combine 구독을 유지하기 위한 저장소 @@ -84,10 +85,12 @@ final class PlaceSelectionViewModel { DispatchQueue.main.async { self.isFetchingNearbyPlace = false switch result { - case .success(let place): - self.nearbyPlace = place - self.updateDistanceText(for: place) + case .success(let fetchedPlace): + self.nearbyPlaceDomain = fetchedPlace + self.nearbyPlace = fetchedPlace.map { PlaceInfo(place: $0) } + self.updateDistanceText(for: fetchedPlace) case .failure: + self.nearbyPlaceDomain = nil self.nearbyPlace = nil self.distanceText = nil } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentInfo.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentInfo.swift index 51c4bf1..510b598 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentInfo.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentInfo.swift @@ -8,10 +8,38 @@ import Foundation struct AppointmentInfo { + let id: String let code: String let title: String let date: Date? let location: AppointmentLocation? let participants: [Participant] + + init(appointment: Appointment) { + id = appointment.id + code = appointment.code + title = appointment.name + date = appointment.dateTime + location = appointment.place.map { AppointmentLocation(place: $0) } + participants = appointment.participants.map { Participant(user: $0) } + } + + /// 약속 생성 화면처럼 아직 Appointment가 만들어지기 전 단계에서 사용 + init( + id: String, + code: String, + title: String, + date: Date?, + location: AppointmentLocation?, + participants: [Participant] + ) { + self.id = id + self.code = code + self.title = title + self.date = date + self.location = location + self.participants = participants + } + } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentLocation.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentLocation.swift index aac6318..a81d17d 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentLocation.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentLocation.swift @@ -6,7 +6,21 @@ // struct AppointmentLocation { + let title: String let address: String let coordinate: Coordinate + + init(place: Place) { + title = place.name + address = place.address + coordinate = place.coordinate + } + + init(title: String, address: String, coordinate: Coordinate) { + self.title = title + self.address = address + self.coordinate = coordinate + } + } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentRouteParticipant.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentRouteParticipant.swift index 8806f5c..97dc3ac 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentRouteParticipant.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentRouteParticipant.swift @@ -14,45 +14,28 @@ struct AppointmentRouteParticipant { let profileImageURL: URL let isMe: Bool let path: [Coordinate] - let transportType: TransportType + let transportName: String + let transportIcon: String + let transportColor: ColorAsset let departureTimeText: String let arrivalTimeText: String - init( - id: String, - nickname: String, - profileImageURL: URL, - isMe: Bool, - path: [Coordinate], - transportType: TransportType, - departureTimeText: String, - arrivalTimeText: String - ) { - self.id = id - self.nickname = nickname - self.profileImageURL = profileImageURL - self.isMe = isMe - self.path = path - self.transportType = transportType - self.departureTimeText = departureTimeText - self.arrivalTimeText = arrivalTimeText - } - init( user: User, route: Route, currentUserID: String ) { - self.init( - id: user.id, - nickname: user.nickname, - profileImageURL: user.profileImage, - isMe: user.id == currentUserID, - path: route.step.flatMap(\.path), - transportType: route.step.last?.transportType ?? user.defaultTransportMode, - departureTimeText: route.departureTime.koreanTimeString, - arrivalTimeText: route.arrivalTime.koreanTimeString - ) + let transport = route.step.last?.transportType ?? user.defaultTransportMode + id = user.id + nickname = user.nickname + profileImageURL = user.profileImage + isMe = user.id == currentUserID + path = route.step.flatMap(\.path) + transportName = transport.name + transportIcon = transport.icon + transportColor = transport.color + departureTimeText = route.departureTime.koreanTimeString + arrivalTimeText = route.arrivalTime.koreanTimeString } } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/ChatBubbleItem.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/ChatBubbleItem.swift index d3f4126..ea81b72 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/ChatBubbleItem.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/ChatBubbleItem.swift @@ -18,7 +18,9 @@ struct ChatBubbleItem: Sendable { let sentAt: Date let contentType: BubbleContentType - enum BubbleContentType: Hashable, Sendable { + /// `nonisolated extension ChatBubbleItem: Hashable`의 `==`에서 비교되므로 + /// 합성 Equatable/Hashable 준수도 nonisolated여야 한다. + nonisolated enum BubbleContentType: Hashable, Sendable { case text case locationShare case placeShare(placeName: String, placeAddress: String, isDuplicate: Bool) diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/ColorAsset.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/ColorAsset.swift new file mode 100644 index 0000000..e455308 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/ColorAsset.swift @@ -0,0 +1,20 @@ +// +// ColorAsset.swift +// WhereAreYou +// +// Created by 이상유 on 2026-08-18. +// + +import Foundation + +enum ColorAsset { + + case green + case yellow + case orange + case indigo + case blue + case red + case brown + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/MyRouteSummary.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/MyRouteSummary.swift index e5b8c12..d88943a 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/MyRouteSummary.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/MyRouteSummary.swift @@ -12,14 +12,14 @@ struct MyRouteSummary { let arrivalTimeText: String let remainingTimeText: String let elapsedTimeText: String - let steps: [RouteStep] + let steps: [RouteStepItem] init(route: Route) { departureTimeText = route.departureTime.koreanTimeString arrivalTimeText = route.arrivalTime.koreanTimeString remainingTimeText = route.arrivalTime.minutesRemainingText elapsedTimeText = route.departureTime.elapsedMinutesText - steps = route.step + steps = route.step.map { RouteStepItem(step: $0) } } } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/Participant.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/Participant.swift index ddf2b6f..fc4b103 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/Participant.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/Participant.swift @@ -6,7 +6,21 @@ // struct Participant { + let id: String let nickname: String let profileImage: String + + init(user: User) { + id = user.id + nickname = user.nickname + profileImage = user.profileImageName + } + + init(id: String, nickname: String, profileImage: String) { + self.id = id + self.nickname = nickname + self.profileImage = profileImage + } + } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/PlaceInfo.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/PlaceInfo.swift index 56c43f9..13c6028 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/PlaceInfo.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/PlaceInfo.swift @@ -5,11 +5,24 @@ // Created by 이상유 on 2026-07-16. // +import Foundation + struct PlaceInfo { let id: String let name: String let address: String - let tag: PlaceType - + let coordinate: Coordinate + let tagTitle: String + let tagColor: ColorAsset + + init(place: Place) { + id = place.id + name = place.name + address = place.address + coordinate = place.coordinate + tagTitle = place.type.title + tagColor = place.type.color + } + } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/RouteItem.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/RouteItem.swift new file mode 100644 index 0000000..da30656 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/RouteItem.swift @@ -0,0 +1,25 @@ +// +// RouteItem.swift +// WhereAreYou +// +// Created by 이상유 on 2026-08-17. +// + +import Foundation + +struct RouteItem { + + let durationText: String + let departureTimeText: String + let arrivalTimeText: String + let steps: [RouteStepItem] + + init(route: Route) { + let totalMinutes = route.arrivalTime.timeIntervalSince(route.departureTime) / 60 + durationText = RouteStepItem.formatDuration(totalMinutes) + departureTimeText = route.departureTime.koreanTimeString + arrivalTimeText = route.arrivalTime.koreanTimeString + steps = route.step.map { RouteStepItem(step: $0) } + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/RouteStepItem.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/RouteStepItem.swift new file mode 100644 index 0000000..ef7a361 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/RouteStepItem.swift @@ -0,0 +1,38 @@ +// +// RouteStepItem.swift +// WhereAreYou +// +// Created by 이상유 on 2026-08-17. +// + +import Foundation + +struct RouteStepItem { + + let departureName: String + let destinationName: String + let estimatedTimeMinutes: Double + let durationText: String + let transportIcon: String + let transportColor: ColorAsset + + init(step: RouteStep) { + departureName = step.departurePoint.name + destinationName = step.destination.name + estimatedTimeMinutes = step.estimatedTime + durationText = Self.formatDuration(step.estimatedTime) + transportIcon = step.transportType.icon + transportColor = step.transportType.color + } + + static func formatDuration(_ minutes: Double) -> String { + let total = Int(minutes) + let hours = total / 60 + let mins = total % 60 + if hours > 0 { + return mins > 0 ? "\(hours)시간 \(mins)분" : "\(hours)시간" + } + return "\(mins)분" + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/SharedPlaceItem.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/SharedPlaceItem.swift index a77109b..90ec618 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/SharedPlaceItem.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/SharedPlaceItem.swift @@ -9,10 +9,21 @@ import Foundation /// 공유된 장소 목록 셀의 표시 정보 struct SharedPlaceItem { + let id: String let placeName: String let placeAddress: String let voterProfileImages: [String] let hasVoted: Bool let sharedAt: Date + + init(sharedPlace: SharedPlace, currentUserID: String) { + id = sharedPlace.id + placeName = sharedPlace.place.name + placeAddress = sharedPlace.place.address + voterProfileImages = sharedPlace.voters.map { $0.profileImageName } + hasVoted = sharedPlace.voters.contains { $0.id == currentUserID } + sharedAt = sharedPlace.sharedAt + } + } diff --git a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewController.swift b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewController.swift index c0b796d..ebbc643 100644 --- a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewController.swift @@ -345,7 +345,7 @@ final class RouteSearchViewController: UIViewController { // MARK: - Update UI - private func updateRouteSection(routes: [Route], selectedIndex: Int) { + private func updateRouteSection(routes: [RouteItem], selectedIndex: Int) { routeCardsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } let hasPlaces = viewModel.departure != nil && viewModel.destination != nil @@ -369,8 +369,8 @@ final class RouteSearchViewController: UIViewController { routeScrollView.isHidden = false - for (index, route) in routes.enumerated() { - let card = RouteCard(route: route, isSelected: index == selectedIndex) + for (index, routeItem) in routes.enumerated() { + let card = RouteCard(routeItem: routeItem, isSelected: index == selectedIndex) card.onTap = { [weak self] in self?.viewModel.selectRoute(at: index) } diff --git a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewModel.swift index 450e73b..995b0b4 100644 --- a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/RouteSearchViewModel.swift @@ -10,14 +10,18 @@ import Combine final class RouteSearchViewModel { - @Published private(set) var departure: Place? - @Published private(set) var destination: Place? + @Published private(set) var departure: PlaceInfo? + @Published private(set) var destination: PlaceInfo? @Published private(set) var departureTime: Date = Date() @Published private(set) var selectedTransportType: TransportType = TransportType.allCases[0] - @Published private(set) var routes: [Route] = [] + @Published private(set) var routes: [RouteItem] = [] @Published private(set) var isLoading = false @Published private(set) var selectedRouteIndex: Int = 0 + private var departureDomain: Place? + private var destinationDomain: Place? + private var routesDomain: [Route] = [] + private let searchRoutesUseCase: SearchRoutesUseCase private let getCurrentLocationUseCase: GetCurrentLocationUseCase @@ -30,12 +34,14 @@ final class RouteSearchViewModel { } func setDeparture(_ place: Place?) { - departure = place + departureDomain = place + departure = place.map { PlaceInfo(place: $0) } searchIfReady() } func setDestination(_ place: Place?) { - destination = place + destinationDomain = place + destination = place.map { PlaceInfo(place: $0) } searchIfReady() } @@ -73,7 +79,8 @@ final class RouteSearchViewModel { } private func searchIfReady() { - guard let departure, let destination else { + guard let departureDomain, let destinationDomain else { + routesDomain = [] routes = [] selectedRouteIndex = 0 return @@ -82,8 +89,8 @@ final class RouteSearchViewModel { isLoading = true searchRoutesUseCase.execute( - departure: departure, - destination: destination, + departure: departureDomain, + destination: destinationDomain, departureTime: departureTime, transportType: selectedTransportType ) { [weak self] result in @@ -93,10 +100,12 @@ final class RouteSearchViewModel { self.selectedRouteIndex = 0 switch result { - case .success(let routes): - self.routes = routes + case .success(let fetchedRoutes): + self.routesDomain = fetchedRoutes + self.routes = fetchedRoutes.map { RouteItem(route: $0) } case .failure: // TODO: 길찾기 API 확인 후 검색 실패 텍스트 설정 필요 + self.routesDomain = [] self.routes = [] } } diff --git a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/SubComponent/RouteCard.swift b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/SubComponent/RouteCard.swift index 5ee3551..644a510 100644 --- a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/SubComponent/RouteCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/SubComponent/RouteCard.swift @@ -11,11 +11,11 @@ final class RouteCard: UIView { var onTap: (() -> Void)? - private let route: Route + private let routeItem: RouteItem private let isSelected: Bool - init(route: Route, isSelected: Bool) { - self.route = route + init(routeItem: RouteItem, isSelected: Bool) { + self.routeItem = routeItem self.isSelected = isSelected super.init(frame: .zero) setUp() @@ -35,7 +35,7 @@ final class RouteCard: UIView { mainStack.addArrangedSubview(makeHeaderRow()) - for step in route.step { + for step in routeItem.steps { mainStack.addArrangedSubview(makeStepView(step)) } @@ -68,13 +68,11 @@ final class RouteCard: UIView { private func makeHeaderRow() -> UIView { let durationLabel = UILabel() - durationLabel.text = formatDuration(route.arrivalTime.timeIntervalSince(route.departureTime) / 60) + durationLabel.text = routeItem.durationText durationLabel.font = .boldPreferredFont(forTextStyle: .headline) let timeRangeLabel = UILabel() - let depStr = route.departureTime.koreanTimeString - let arrStr = route.arrivalTime.koreanTimeString - timeRangeLabel.text = "\(depStr) - \(arrStr)" + timeRangeLabel.text = "\(routeItem.departureTimeText) - \(routeItem.arrivalTimeText)" timeRangeLabel.font = .preferredFont(forTextStyle: .footnote) timeRangeLabel.textColor = .secondaryLabel @@ -86,19 +84,19 @@ final class RouteCard: UIView { // MARK: - Step - private func makeStepView(_ step: RouteStep) -> UIView { + private func makeStepView(_ step: RouteStepItem) -> UIView { let container = UIView() - let icon = UIImageView(image: UIImage(systemName: step.transportType.icon)) - icon.tintColor = step.transportType.color + let icon = UIImageView(image: UIImage(systemName: step.transportIcon)) + icon.tintColor = step.transportColor.uiColor icon.contentMode = .scaleAspectFit let nameLabel = UILabel() - nameLabel.text = "\(step.departurePoint.name) > \(step.destination.name)" + nameLabel.text = "\(step.departureName) > \(step.destinationName)" nameLabel.font = .boldPreferredFont(forTextStyle: .subheadline) let durationLabel = UILabel() - durationLabel.text = formatDuration(step.estimatedTime) + durationLabel.text = step.durationText durationLabel.font = .preferredFont(forTextStyle: .footnote) durationLabel.textColor = .secondaryLabel @@ -153,16 +151,4 @@ final class RouteCard: UIView { return outer } - // MARK: - Formatting - - private func formatDuration(_ minutes: Double) -> String { - let total = Int(minutes) - let hours = total / 60 - let mins = total % 60 - if hours > 0 { - return mins > 0 ? "\(hours)시간 \(mins)분" : "\(hours)시간" - } - return "\(mins)분" - } - } diff --git a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/SubComponent/TransportTypeSelectionBox.swift b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/SubComponent/TransportTypeSelectionBox.swift index 641981a..f50e0c0 100644 --- a/WhereAreYou/WhereAreYou/Presentation/RouteSearch/SubComponent/TransportTypeSelectionBox.swift +++ b/WhereAreYou/WhereAreYou/Presentation/RouteSearch/SubComponent/TransportTypeSelectionBox.swift @@ -62,7 +62,7 @@ final class TransportTypeSelectionBox: UIView { config.cornerStyle = .fixed config.background.cornerRadius = 12 config.background.strokeWidth = 1.5 - config.background.strokeColor = type.color + config.background.strokeColor = type.color.uiColor config.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 8, bottom: 12, trailing: 8) config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in var out = incoming @@ -79,11 +79,11 @@ final class TransportTypeSelectionBox: UIView { for (type, button) in buttons { var config = button.configuration ?? .filled() if type == selectedType { - config.baseBackgroundColor = type.color.withAlphaComponent(0.8) + config.baseBackgroundColor = type.color.uiColor.withAlphaComponent(0.8) config.baseForegroundColor = .white } else { config.baseBackgroundColor = .white - config.baseForegroundColor = type.color + config.baseForegroundColor = type.color.uiColor } button.configuration = config }