From 3249002f0486cd4f59cd23e49c266702c074f85a Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Mon, 12 Oct 2026 19:44:02 +0900 Subject: [PATCH 1/4] =?UTF-8?q?REFACTOR:=20Appointment=EC=9D=98=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20=ED=95=84=EB=93=9C=20placeVoteSta?= =?UTF-8?q?tus=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Mock/MockAppointmentCreationRepository.swift | 3 +-- .../Data/Mock/MockAppointmentDetailRepository.swift | 3 +-- .../Data/Mock/MockAppointmentInfoRepository.swift | 6 ++---- WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift | 1 - 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentCreationRepository.swift b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentCreationRepository.swift index c2a713d..c640bea 100644 --- a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentCreationRepository.swift +++ b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentCreationRepository.swift @@ -23,8 +23,7 @@ final class MockAppointmentCreationRepository: AppointmentCreationRepository { dateTime: date ?? Date(), place: place, participants: [], - routes: [:], - placeVoteStatus: [:] + routes: [:] ) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { diff --git a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentDetailRepository.swift b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentDetailRepository.swift index 42bd941..883b345 100644 --- a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentDetailRepository.swift +++ b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentDetailRepository.swift @@ -154,8 +154,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository { dateTime: myTimelineBase.addingTimeInterval(25 * 60), place: place, participants: users, - routes: routes, - placeVoteStatus: [:] + routes: routes ) } diff --git a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentInfoRepository.swift b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentInfoRepository.swift index 5f0c98d..e6f7a88 100644 --- a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentInfoRepository.swift +++ b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentInfoRepository.swift @@ -43,8 +43,7 @@ final class MockAppointmentInfoRepository: AppointmentInfoRepository { dateTime: date ?? current.dateTime, place: place ?? current.place, participants: current.participants, - routes: current.routes, - placeVoteStatus: current.placeVoteStatus + routes: current.routes ) storedAppointments[appointmentID] = updated @@ -99,8 +98,7 @@ final class MockAppointmentInfoRepository: AppointmentInfoRepository { dateTime: Date().addingTimeInterval(3600), place: place, participants: [currentUser, user2, user3], - routes: [:], - placeVoteStatus: [:] + routes: [:] ) } diff --git a/WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift b/WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift index ec8fafe..b43b7ef 100644 --- a/WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift +++ b/WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift @@ -16,6 +16,5 @@ struct Appointment { let place: Place? let participants: [User] let routes: [String: Route] - let placeVoteStatus: [String: [User]] } From 4d2e346bd268ff3e10e3333727b85daacbe2363e Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Mon, 12 Oct 2026 19:44:10 +0900 Subject: [PATCH 2/4] =?UTF-8?q?REFACTOR:=20=EC=95=BD=EC=86=8D=20Repository?= =?UTF-8?q?=EB=A5=BC=20=EC=95=BD=EC=86=8D=20=EB=AC=B8=EC=84=9C=C2=B7?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=8B=A8=EC=9C=84=202=EA=B0=9C=EB=A1=9C?= =?UTF-8?q?=20=EC=9E=AC=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppDIContainer+Register.swift | 11 +- .../ScreenFactory+AppointmentCreation.swift | 2 +- .../ScreenFactory+AppointmentInfo.swift | 2 +- .../ScreenFactory+AppointmentRoute.swift | 4 +- .../ScreenFactory/ScreenFactory+Chat.swift | 12 +- .../ScreenFactory/ScreenFactory+Home.swift | 2 +- .../MockAppointmentCreationRepository.swift | 34 ----- .../Mock/MockAppointmentInfoRepository.swift | 105 ------------- .../Data/Mock/MockAppointmentRepository.swift | 138 +++++++++++++++++ ...t => MockAppointmentRouteRepository.swift} | 142 ++++-------------- ...ViewController+LiveActivityDebugMenu.swift | 10 +- .../StartRouteLiveActivityUseCase.swift | 18 +-- .../Domain/Entity/Appointment.swift | 1 - .../AppointmentCreationRepository.swift | 19 --- .../AppointmentDetailRepository.swift | 22 --- .../AppointmentInfoRepository.swift | 22 --- .../Repository/AppointmentRepository.swift | 28 ++++ .../AppointmentRouteRepository.swift | 16 ++ .../CreateAppointmentUseCase.swift | 18 +-- .../FetchAppointmentInfoUseCase.swift | 11 +- .../GetAppointmentDetailUseCase.swift | 29 +++- .../Appointment/JoinAppointmentUseCase.swift | 11 +- .../UpdateAppointmentInfoUseCase.swift | 18 +-- .../AppointmentCreationViewModel.swift | 30 ++-- .../AppointmentInfoViewModel.swift | 32 ++-- .../Presentation/Home/HomeViewModel.swift | 8 +- .../AppointmentRouteViewModel.swift | 21 ++- 27 files changed, 326 insertions(+), 440 deletions(-) delete mode 100644 WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentCreationRepository.swift delete mode 100644 WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentInfoRepository.swift create mode 100644 WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentRepository.swift rename WhereAreYou/WhereAreYou/Data/Mock/{MockAppointmentDetailRepository.swift => MockAppointmentRouteRepository.swift} (67%) delete mode 100644 WhereAreYou/WhereAreYou/Domain/Repository/AppointmentCreationRepository.swift delete mode 100644 WhereAreYou/WhereAreYou/Domain/Repository/AppointmentDetailRepository.swift delete mode 100644 WhereAreYou/WhereAreYou/Domain/Repository/AppointmentInfoRepository.swift create mode 100644 WhereAreYou/WhereAreYou/Domain/Repository/AppointmentRepository.swift create mode 100644 WhereAreYou/WhereAreYou/Domain/Repository/AppointmentRouteRepository.swift diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift index c586d95..b88d456 100644 --- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift +++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift @@ -46,10 +46,13 @@ extension DIContainer { register(LocationRepository.self, instance: coreLocationRepository) register(LocationPermissionRepository.self, instance: coreLocationRepository) - // 약속 생성/조회 - register(AppointmentCreationRepository.self) { MockAppointmentCreationRepository() } - register(AppointmentDetailRepository.self) { MockAppointmentDetailRepository() } - register(AppointmentInfoRepository.self) { MockAppointmentInfoRepository() } + // 약속 + register(AppointmentRepository.self) { + MockAppointmentRepository(authRepository: self.resolve(AuthRepository.self)) + } + register(AppointmentRouteRepository.self) { + MockAppointmentRouteRepository(authRepository: self.resolve(AuthRepository.self)) + } // 채팅 register(ChatRepository.self) { MockChatRepository() } diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentCreation.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentCreation.swift index 94f55ec..b942733 100644 --- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentCreation.swift +++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentCreation.swift @@ -10,7 +10,7 @@ extension ScreenFactory { func makeAppointmentCreationViewController() -> AppointmentCreationViewController { let viewModel = AppointmentCreationViewModel( createAppointmentUseCase: CreateAppointmentUseCase( - repository: container.resolve(AppointmentCreationRepository.self) + repository: container.resolve(AppointmentRepository.self) ) ) return AppointmentCreationViewController(viewModel) diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentInfo.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentInfo.swift index fea22b9..b60db04 100644 --- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentInfo.swift +++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentInfo.swift @@ -8,7 +8,7 @@ extension ScreenFactory { func makeAppointmentInfoViewController(appointmentID: String) -> AppointmentInfoViewController { - let repository = container.resolve(AppointmentInfoRepository.self) + let repository = container.resolve(AppointmentRepository.self) let viewModel = AppointmentInfoViewModel( appointmentID: appointmentID, fetchAppointmentInfoUseCase: FetchAppointmentInfoUseCase(repository: repository), diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentRoute.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentRoute.swift index c55e7e4..2c249cd 100644 --- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentRoute.swift +++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+AppointmentRoute.swift @@ -11,7 +11,9 @@ extension ScreenFactory { let viewModel = AppointmentRouteViewModel( appointmentID: appointmentID, getAppointmentDetailUseCase: GetAppointmentDetailUseCase( - repository: container.resolve(AppointmentDetailRepository.self) + appointmentRepository: container.resolve(AppointmentRepository.self), + appointmentRouteRepository: container.resolve(AppointmentRouteRepository.self), + authRepository: container.resolve(AuthRepository.self) ) ) return AppointmentRouteViewController(viewModel: viewModel) diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+Chat.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+Chat.swift index 47a13bb..b3cdf7f 100644 --- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+Chat.swift +++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+Chat.swift @@ -40,16 +40,16 @@ extension ScreenFactory { appointmentID: String, completion: @escaping (Result) -> Void ) { - let repository = container.resolve(AppointmentInfoRepository.self) - FetchAppointmentInfoUseCase(repository: repository).execute(appointmentID: appointmentID) { [weak self] result in - guard let self else { return } - switch result { - case .success(let appointment): + let useCase = FetchAppointmentInfoUseCase(repository: container.resolve(AppointmentRepository.self)) + Task { @MainActor [weak self] in + do { + let appointment = try await useCase.execute(appointmentID: appointmentID) + guard let self else { return } let chatViewController = self.makeChatViewController( appointmentInfo: AppointmentInfo(appointment: appointment) ) completion(.success(chatViewController)) - case .failure(let error): + } catch { completion(.failure(error)) } } diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+Home.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+Home.swift index a95fe2b..be3b05e 100644 --- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+Home.swift +++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/ScreenFactory/ScreenFactory+Home.swift @@ -10,7 +10,7 @@ extension ScreenFactory { func makeHomeViewController() -> HomeViewController { let viewModel = HomeViewModel( joinAppointmentUseCase: JoinAppointmentUseCase( - repository: container.resolve(AppointmentDetailRepository.self) + repository: container.resolve(AppointmentRepository.self) ) ) return HomeViewController(viewModel: viewModel) diff --git a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentCreationRepository.swift b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentCreationRepository.swift deleted file mode 100644 index c640bea..0000000 --- a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentCreationRepository.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// 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: [:] - ) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - completion(.success(appointment)) - } - } - -} diff --git a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentInfoRepository.swift b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentInfoRepository.swift deleted file mode 100644 index e6f7a88..0000000 --- a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentInfoRepository.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// MockAppointmentInfoRepository.swift -// WhereAreYou -// -// Created by 이상유 on 2026-08-06. -// - -import Foundation - -final class MockAppointmentInfoRepository: AppointmentInfoRepository { - - private var storedAppointments: [String: Appointment] = [:] - - func fetchAppointmentInfo( - appointmentID: String, - completion: @escaping (Result) -> Void - ) { - if storedAppointments[appointmentID] == nil { - storedAppointments[appointmentID] = Self.makeMockAppointment(appointmentID: appointmentID) - } - let appointment = storedAppointments[appointmentID]! - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - completion(.success(appointment)) - } - } - - func updateAppointmentInfo( - appointmentID: String, - name: String?, - date: Date?, - place: Place?, - completion: @escaping (Result) -> Void - ) { - guard let current = storedAppointments[appointmentID] else { - completion(.failure(NSError(domain: "MockAppointmentInfoRepository", code: 404))) - return - } - - let updated = Appointment( - id: current.id, - code: current.code, - name: name ?? current.name, - dateTime: date ?? current.dateTime, - place: place ?? current.place, - participants: current.participants, - routes: current.routes - ) - storedAppointments[appointmentID] = updated - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - completion(.success(updated)) - } - } - - private static let currentUser = User( - id: "me", - nickname: "나", - profileImage: "otter", - defaultTransportMode: .transit, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ) - - private static let user2 = User( - id: "user2", - nickname: "김길동", - profileImage: "shark", - defaultTransportMode: .transit, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ) - - private static let user3 = User( - id: "user3", - nickname: "홍길동", - profileImage: "turtle", - defaultTransportMode: .transit, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ) - - private static func makeMockAppointment(appointmentID: String) -> Appointment { - let place = Place( - id: "gangnam", - name: "강남역", - address: "서울 강남구 강남대로 396", - coordinate: Coordinate(latitude: 37.4979, longitude: 127.0276), - type: .subway - ) - - return Appointment( - id: appointmentID, - code: "ABCD1234", - name: "강남역 모임", - dateTime: Date().addingTimeInterval(3600), - place: place, - participants: [currentUser, user2, user3], - routes: [:] - ) - } - -} diff --git a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentRepository.swift b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentRepository.swift new file mode 100644 index 0000000..5c52901 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentRepository.swift @@ -0,0 +1,138 @@ +// +// MockAppointmentRepository.swift +// WhereAreYou +// +// Created by 이상유 on 2026-08-06. +// + +import Foundation + +/// 서버 연결 전 약속 저장소 — 고정 약속 하나를 돌려주고, 정보 수정은 인스턴스 안에만 반영한다. +/// "나"는 로그인한 사용자 ID로 넣어 경로·참여자 화면에서 내 항목을 찾을 수 있게 한다 +final class MockAppointmentRepository: AppointmentRepository { + + /// true면 약속 시각을 5분 전으로 당긴다 — 약속 시간이 지난 상태의 라이브 액티비티 확인용. + /// 내 경로도 같이 당겨야 해서 MockAppointmentRouteRepository가 함께 읽는다 + static var startsAfterAppointmentTime = false + static let place = Place( + id: "place_starbucks_gangnam", + name: "스타벅스 강남역점", + address: "서울 강남구 강남대로 396", + coordinate: Coordinate(latitude: 37.4979, longitude: 127.0276), + type: .cafe + ) + /// 로그인 정보가 없을 때 쓰는 내 ID + static let fallbackCurrentUserID = "user_me" + private static let mockCode = "MOCK1234" + + private let authRepository: AuthRepository + private var storedAppointments: [String: Appointment] = [:] + + init(authRepository: AuthRepository) { + self.authRepository = authRepository + } + + func createAppointment(name: String, dateTime: Date, place: Place?) async throws -> Appointment { + try await Task.sleep(for: .milliseconds(500)) + let appointment = Appointment( + id: UUID().uuidString, + code: String(UUID().uuidString.prefix(8)), + name: name, + dateTime: dateTime, + place: place, + participants: [] + ) + storedAppointments[appointment.id] = appointment + return appointment + } + + func joinAppointment(code: String) async throws -> Appointment { + try await Task.sleep(for: .milliseconds(500)) + guard code == Self.mockCode else { + throw AppError.notFound + } + return storedAppointment(appointmentID: "appointment_joined") + } + + func fetchAppointment(appointmentID: String) async throws -> Appointment { + try await Task.sleep(for: .milliseconds(300)) + return storedAppointment(appointmentID: appointmentID) + } + + func updateAppointment( + appointmentID: String, + name: String?, + dateTime: Date?, + place: Place? + ) async throws -> Appointment { + try await Task.sleep(for: .milliseconds(200)) + guard let current = storedAppointments[appointmentID] else { + throw AppError.notFound + } + + let updated = Appointment( + id: current.id, + code: current.code, + name: name ?? current.name, + dateTime: dateTime ?? current.dateTime, + place: place ?? current.place, + participants: current.participants + ) + storedAppointments[appointmentID] = updated + return updated + } + + private func storedAppointment(appointmentID: String) -> Appointment { + if let stored = storedAppointments[appointmentID] { + return stored + } + let appointment = makeMockAppointment(appointmentID: appointmentID) + storedAppointments[appointmentID] = appointment + return appointment + } + + private func makeMockAppointment(appointmentID: String) -> Appointment { + let timelineBase = Self.startsAfterAppointmentTime ? Date().addingTimeInterval(-30 * 60) : Date() + + return Appointment( + id: appointmentID, + code: Self.mockCode, + name: "고기굽는방앗간 이수역점", + dateTime: timelineBase.addingTimeInterval(25 * 60), + place: Self.place, + participants: makeParticipants() + ) + } + + private func makeParticipants() -> [User] { + let me = User( + id: authRepository.currentUserID ?? Self.fallbackCurrentUserID, + nickname: "나", + profileImage: "shark", + defaultTransportMode: .car, + locationSharingScope: .onlyDuringAppointment, + isNotificationEnabled: true, + appointmentsNotification: [:] + ) + let others: [(id: String, nickname: String, profileImage: String, transportMode: TransportType)] = [ + ("user_minji", "김민지", "turtle", .transit), + ("user_seoyeon", "이서연", "shark", .car), + ("user_junwoo", "박준우", "turtle", .transit), + ("user_gildong", "최길동", "shark", .walk), + ("user_subin", "정수빈", "turtle", .car), + ] + + return [me] + others.map { + User( + id: $0.id, + nickname: $0.nickname, + profileImage: $0.profileImage, + defaultTransportMode: $0.transportMode, + locationSharingScope: .onlyDuringAppointment, + isNotificationEnabled: true, + appointmentsNotification: [:] + ) + } + } + +} diff --git a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentDetailRepository.swift b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentRouteRepository.swift similarity index 67% rename from WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentDetailRepository.swift rename to WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentRouteRepository.swift index 883b345..42cddbc 100644 --- a/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentDetailRepository.swift +++ b/WhereAreYou/WhereAreYou/Data/Mock/MockAppointmentRouteRepository.swift @@ -1,5 +1,5 @@ // -// MockAppointmentDetailRepository.swift +// MockAppointmentRouteRepository.swift // WhereAreYou // // Created by 김성훈 on 7/31/26. @@ -7,155 +7,67 @@ import Foundation -final class MockAppointmentDetailRepository: AppointmentDetailRepository { - - func fetchAppointment( - appointmentID: String, - completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void - ) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - let appointment = Self.makeMockAppointment(appointmentID: appointmentID) - completion(.success((appointment: appointment, currentUserID: Self.currentUserID))) - } - } - - func fetchAppointment( - code: String, - completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void - ) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - guard code == Self.mockCode else { - completion(.failure(AppError.notFound)) - return - } - let appointment = Self.makeMockAppointment(appointmentID: "appointment_joined") - completion(.success((appointment: appointment, currentUserID: Self.currentUserID))) - } - } - - private static let currentUserID = "user_me" +/// 서버 연결 전 참여자 경로 저장소 — MockAppointmentRepository의 참여자와 약속 장소에 맞춘 고정 경로를 돌려준다 +final class MockAppointmentRouteRepository: AppointmentRouteRepository { /// 내 경로 시나리오 — DEBUG 라이브 액티비티 시작 메뉴에서 바꿔 가며 확인한다 static var myRouteScenario: MockMyRouteScenario = .subwayTransfer - /// true면 약속 시각을 5분 전으로 당긴다 — 약속 시간이 지난 상태의 라이브 액티비티 확인용 - static var startsAfterAppointmentTime = false - private static let mockCode = "MOCK1234" - private static func makeMockAppointment(appointmentID: String) -> Appointment { - let place = Place( - id: "place_starbucks_gangnam", - name: "스타벅스 강남역점", - address: "서울 강남구 강남대로 396", - coordinate: Coordinate(latitude: 37.4979, longitude: 127.0276), - type: .cafe - ) + private let authRepository: AuthRepository + + init(authRepository: AuthRepository) { + self.authRepository = authRepository + } + + func fetchRoutes(appointmentID: String) async throws -> [String: Route] { + try await Task.sleep(for: .milliseconds(500)) + return makeMockRoutes() + } + private func makeMockRoutes() -> [String: Route] { + let place = MockAppointmentRepository.place + let currentUserID = authRepository.currentUserID ?? MockAppointmentRepository.fallbackCurrentUserID let now = Date() // 약속 시각과 내 경로만 당긴다. 다른 참여자 경로는 지도 표시용이라 그대로 둔다 - let myTimelineBase = startsAfterAppointmentTime ? now.addingTimeInterval(-30 * 60) : now + let myTimelineBase = MockAppointmentRepository.startsAfterAppointmentTime + ? now.addingTimeInterval(-30 * 60) + : now - let users: [User] = [ - User( - id: currentUserID, - nickname: "나", - profileImage: "shark", - defaultTransportMode: .car, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ), - User( - id: "user_minji", - nickname: "김민지", - profileImage: "turtle", - defaultTransportMode: .transit, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ), - User( - id: "user_seoyeon", - nickname: "이서연", - profileImage: "shark", - defaultTransportMode: .car, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ), - User( - id: "user_junwoo", - nickname: "박준우", - profileImage: "turtle", - defaultTransportMode: .transit, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ), - User( - id: "user_gildong", - nickname: "최길동", - profileImage: "shark", - defaultTransportMode: .walk, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ), - User( - id: "user_subin", - nickname: "정수빈", - profileImage: "turtle", - defaultTransportMode: .car, - locationSharingScope: .onlyDuringAppointment, - isNotificationEnabled: true, - appointmentsNotification: [:] - ), - ] - - let routes: [String: Route] = [ - currentUserID: makeMyRoute( + return [ + currentUserID: Self.makeMyRoute( to: place, departureTime: myTimelineBase.addingTimeInterval(1 * 60) ), - "user_minji": makeRoute( + "user_minji": Self.makeRoute( to: place, departureTime: now.addingTimeInterval(-40 * 60), arrivalTime: now.addingTimeInterval(8 * 60), start: Coordinate(latitude: 37.5040, longitude: 127.0350), transportType: .transit ), - "user_seoyeon": makeRoute( + "user_seoyeon": Self.makeRoute( to: place, departureTime: now.addingTimeInterval(-38 * 60), arrivalTime: now.addingTimeInterval(7 * 60), start: Coordinate(latitude: 37.4990, longitude: 127.0120), transportType: .car ), - "user_junwoo": makeRoute( + "user_junwoo": Self.makeRoute( to: place, departureTime: now.addingTimeInterval(-30 * 60), arrivalTime: now.addingTimeInterval(10 * 60), start: Coordinate(latitude: 37.4900, longitude: 127.0300), transportType: .transit ), - "user_gildong": makeRoute( + "user_gildong": Self.makeRoute( to: place, departureTime: now.addingTimeInterval(-25 * 60), arrivalTime: now.addingTimeInterval(15 * 60), start: Coordinate(latitude: 37.4965, longitude: 127.0230), transportType: .walk ), - "user_subin": makeRoute( + "user_subin": Self.makeRoute( to: place, departureTime: now.addingTimeInterval(-20 * 60), arrivalTime: now.addingTimeInterval(11 * 60), start: Coordinate(latitude: 37.5010, longitude: 127.0400), transportType: .car ), ] - - return Appointment( - id: appointmentID, - code: Self.mockCode, - name: "고기굽는방앗간 이수역점", - dateTime: myTimelineBase.addingTimeInterval(25 * 60), - place: place, - participants: users, - routes: routes - ) } private static func makeRoute( @@ -336,7 +248,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository { } -/// MockAppointmentDetailRepository가 돌려주는 내 경로 종류 +/// MockAppointmentRouteRepository가 돌려주는 내 경로 종류 enum MockMyRouteScenario: CaseIterable { case subwayTransfer diff --git a/WhereAreYou/WhereAreYou/DebugLiveActivity/ChatViewController+LiveActivityDebugMenu.swift b/WhereAreYou/WhereAreYou/DebugLiveActivity/ChatViewController+LiveActivityDebugMenu.swift index dccb116..43b20bd 100644 --- a/WhereAreYou/WhereAreYou/DebugLiveActivity/ChatViewController+LiveActivityDebugMenu.swift +++ b/WhereAreYou/WhereAreYou/DebugLiveActivity/ChatViewController+LiveActivityDebugMenu.swift @@ -18,8 +18,8 @@ extension ChatViewController { let makeScenarioActions: (Bool) -> [UIAction] = { startsAfterAppointmentTime in MockMyRouteScenario.allCases.map { scenario in UIAction(title: scenario.title) { [weak self] _ in - MockAppointmentDetailRepository.myRouteScenario = scenario - MockAppointmentDetailRepository.startsAfterAppointmentTime = startsAfterAppointmentTime + MockAppointmentRouteRepository.myRouteScenario = scenario + MockAppointmentRepository.startsAfterAppointmentTime = startsAfterAppointmentTime self?.startLiveActivity(appointmentID: appointmentID) } } @@ -42,7 +42,11 @@ extension ChatViewController { private func startLiveActivity(appointmentID: String) { let container = DIContainer.shared let useCase = StartRouteLiveActivityUseCase( - appointmentDetailRepository: container.resolve(AppointmentDetailRepository.self), + getAppointmentDetailUseCase: GetAppointmentDetailUseCase( + appointmentRepository: container.resolve(AppointmentRepository.self), + appointmentRouteRepository: container.resolve(AppointmentRouteRepository.self), + authRepository: container.resolve(AuthRepository.self) + ), liveActivityRepository: ActivityKitRouteLiveActivityRepository(), permissionRepository: container.resolve(LiveActivityPermissionRepository.self) ) diff --git a/WhereAreYou/WhereAreYou/DebugLiveActivity/StartRouteLiveActivityUseCase.swift b/WhereAreYou/WhereAreYou/DebugLiveActivity/StartRouteLiveActivityUseCase.swift index c609ce0..7a03d23 100644 --- a/WhereAreYou/WhereAreYou/DebugLiveActivity/StartRouteLiveActivityUseCase.swift +++ b/WhereAreYou/WhereAreYou/DebugLiveActivity/StartRouteLiveActivityUseCase.swift @@ -13,16 +13,16 @@ import Foundation /// 실시간 현황이 꺼져 있으면 permissionDenied, 내 경로나 약속 장소가 없으면 notFound final class StartRouteLiveActivityUseCase { - private let appointmentDetailRepository: AppointmentDetailRepository + private let getAppointmentDetailUseCase: GetAppointmentDetailUseCase private let liveActivityRepository: RouteLiveActivityStarting private let permissionRepository: LiveActivityPermissionRepository init( - appointmentDetailRepository: AppointmentDetailRepository, + getAppointmentDetailUseCase: GetAppointmentDetailUseCase, liveActivityRepository: RouteLiveActivityStarting, permissionRepository: LiveActivityPermissionRepository ) { - self.appointmentDetailRepository = appointmentDetailRepository + self.getAppointmentDetailUseCase = getAppointmentDetailUseCase self.liveActivityRepository = liveActivityRepository self.permissionRepository = permissionRepository } @@ -32,8 +32,8 @@ final class StartRouteLiveActivityUseCase { throw AppError.permissionDenied } - let (appointment, currentUserID) = try await fetchAppointment(appointmentID: appointmentID) - guard let route = appointment.routes[currentUserID], let place = appointment.place else { + let (appointment, routes, currentUserID) = try await getAppointmentDetailUseCase.execute(appointmentID: appointmentID) + guard let route = routes[currentUserID], let place = appointment.place else { throw AppError.notFound } @@ -52,13 +52,5 @@ final class StartRouteLiveActivityUseCase { ) } - private func fetchAppointment(appointmentID: String) async throws -> (Appointment, String) { - try await withCheckedThrowingContinuation { continuation in - appointmentDetailRepository.fetchAppointment(appointmentID: appointmentID) { result in - continuation.resume(with: result.map { ($0.appointment, $0.currentUserID) }) - } - } - } - } #endif diff --git a/WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift b/WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift index b43b7ef..cc7a3ba 100644 --- a/WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift +++ b/WhereAreYou/WhereAreYou/Domain/Entity/Appointment.swift @@ -15,6 +15,5 @@ struct Appointment { let dateTime: Date let place: Place? let participants: [User] - let routes: [String: Route] } diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentCreationRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentCreationRepository.swift deleted file mode 100644 index 5126ef7..0000000 --- a/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentCreationRepository.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// 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/Repository/AppointmentDetailRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentDetailRepository.swift deleted file mode 100644 index 06b0992..0000000 --- a/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentDetailRepository.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AppointmentDetailRepository.swift -// WhereAreYou -// -// Created by 김성훈 on 7/31/26. -// - -import Foundation - -protocol AppointmentDetailRepository { - - func fetchAppointment( - appointmentID: String, - completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void - ) - - func fetchAppointment( - code: String, - completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void - ) - -} diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentInfoRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentInfoRepository.swift deleted file mode 100644 index 96f8150..0000000 --- a/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentInfoRepository.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AppointmentInfoRepository.swift -// WhereAreYou -// -// Created by 이상유 on 2026-08-06. -// - -import Foundation - -protocol AppointmentInfoRepository { - func fetchAppointmentInfo( - appointmentID: String, - completion: @escaping (Result) -> Void - ) - func updateAppointmentInfo( - appointmentID: String, - name: String?, - date: Date?, - place: Place?, - completion: @escaping (Result) -> Void - ) -} diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentRepository.swift new file mode 100644 index 0000000..479c17f --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentRepository.swift @@ -0,0 +1,28 @@ +// +// AppointmentRepository.swift +// WhereAreYou +// +// Created by 이상유 on 2026-08-06. +// + +import Foundation + +/// 약속 문서 저장소 — 생성·참여(서버 처리)와 조회·정보 수정. 참여자별 경로는 AppointmentRouteRepository가 맡는다 +protocol AppointmentRepository { + + func createAppointment(name: String, dateTime: Date, place: Place?) async throws -> Appointment + + /// 코드에 해당하는 약속이 없으면 AppError.notFound + func joinAppointment(code: String) async throws -> Appointment + + func fetchAppointment(appointmentID: String) async throws -> Appointment + + /// nil인 항목은 변경하지 않는다 + func updateAppointment( + appointmentID: String, + name: String?, + dateTime: Date?, + place: Place? + ) async throws -> Appointment + +} diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentRouteRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentRouteRepository.swift new file mode 100644 index 0000000..325048e --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/Repository/AppointmentRouteRepository.swift @@ -0,0 +1,16 @@ +// +// AppointmentRouteRepository.swift +// WhereAreYou +// +// Created by 이상유 on 2026-10-12. +// + +import Foundation + +/// 약속 참여자별 경로 저장소 — 약속 문서와 따로 저장되는 routes 하위 컬렉션을 다룬다 +protocol AppointmentRouteRepository { + + /// 참여자 ID → 경로. 경로를 아직 정하지 않은 참여자는 빠진다 + func fetchRoutes(appointmentID: String) async throws -> [String: Route] + +} diff --git a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/CreateAppointmentUseCase.swift b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/CreateAppointmentUseCase.swift index c7460a3..77319aa 100644 --- a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/CreateAppointmentUseCase.swift +++ b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/CreateAppointmentUseCase.swift @@ -9,24 +9,14 @@ import Foundation final class CreateAppointmentUseCase { - private let repository: AppointmentCreationRepository + private let repository: AppointmentRepository - init(repository: AppointmentCreationRepository) { + init(repository: AppointmentRepository) { 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 - ) + func execute(name: String, dateTime: Date, place: Place?) async throws -> Appointment { + try await repository.createAppointment(name: name, dateTime: dateTime, place: place) } } diff --git a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/FetchAppointmentInfoUseCase.swift b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/FetchAppointmentInfoUseCase.swift index 44a0c61..4e060bf 100644 --- a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/FetchAppointmentInfoUseCase.swift +++ b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/FetchAppointmentInfoUseCase.swift @@ -7,17 +7,14 @@ final class FetchAppointmentInfoUseCase { - private let repository: AppointmentInfoRepository + private let repository: AppointmentRepository - init(repository: AppointmentInfoRepository) { + init(repository: AppointmentRepository) { self.repository = repository } - func execute( - appointmentID: String, - completion: @escaping (Result) -> Void - ) { - repository.fetchAppointmentInfo(appointmentID: appointmentID, completion: completion) + func execute(appointmentID: String) async throws -> Appointment { + try await repository.fetchAppointment(appointmentID: appointmentID) } } diff --git a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/GetAppointmentDetailUseCase.swift b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/GetAppointmentDetailUseCase.swift index 85f24ef..020c61b 100644 --- a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/GetAppointmentDetailUseCase.swift +++ b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/GetAppointmentDetailUseCase.swift @@ -5,19 +5,34 @@ // Created by 김성훈 on 7/31/26. // +/// 경로 지도용 약속 상세 — 약속 문서와 참여자별 경로를 함께 조회하고 현재 사용자 ID를 붙인다. +/// 로그인 정보가 없으면 AppError.notAuthenticated final class GetAppointmentDetailUseCase { - private let repository: AppointmentDetailRepository + private let appointmentRepository: AppointmentRepository + private let appointmentRouteRepository: AppointmentRouteRepository + private let authRepository: AuthRepository - init(repository: AppointmentDetailRepository) { - self.repository = repository + init( + appointmentRepository: AppointmentRepository, + appointmentRouteRepository: AppointmentRouteRepository, + authRepository: AuthRepository + ) { + self.appointmentRepository = appointmentRepository + self.appointmentRouteRepository = appointmentRouteRepository + self.authRepository = authRepository } func execute( - appointmentID: String, - completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void - ) { - repository.fetchAppointment(appointmentID: appointmentID, completion: completion) + appointmentID: String + ) async throws -> (appointment: Appointment, routes: [String: Route], currentUserID: String) { + guard let currentUserID = authRepository.currentUserID else { + throw AppError.notAuthenticated + } + + async let appointment = appointmentRepository.fetchAppointment(appointmentID: appointmentID) + async let routes = appointmentRouteRepository.fetchRoutes(appointmentID: appointmentID) + return try await (appointment, routes, currentUserID) } } diff --git a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/JoinAppointmentUseCase.swift b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/JoinAppointmentUseCase.swift index 16100f6..85e9f51 100644 --- a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/JoinAppointmentUseCase.swift +++ b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/JoinAppointmentUseCase.swift @@ -7,17 +7,14 @@ final class JoinAppointmentUseCase { - private let repository: AppointmentDetailRepository + private let repository: AppointmentRepository - init(repository: AppointmentDetailRepository) { + init(repository: AppointmentRepository) { self.repository = repository } - func execute( - code: String, - completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void - ) { - repository.fetchAppointment(code: code, completion: completion) + func execute(code: String) async throws -> Appointment { + try await repository.joinAppointment(code: code) } } diff --git a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/UpdateAppointmentInfoUseCase.swift b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/UpdateAppointmentInfoUseCase.swift index efe75be..2d334f4 100644 --- a/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/UpdateAppointmentInfoUseCase.swift +++ b/WhereAreYou/WhereAreYou/Domain/UseCase/Appointment/UpdateAppointmentInfoUseCase.swift @@ -9,25 +9,23 @@ import Foundation final class UpdateAppointmentInfoUseCase { - private let repository: AppointmentInfoRepository + private let repository: AppointmentRepository - init(repository: AppointmentInfoRepository) { + init(repository: AppointmentRepository) { self.repository = repository } func execute( appointmentID: String, name: String?, - date: Date?, - place: Place?, - completion: @escaping (Result) -> Void - ) { - repository.updateAppointmentInfo( + dateTime: Date?, + place: Place? + ) async throws -> Appointment { + try await repository.updateAppointment( appointmentID: appointmentID, name: name, - date: date, - place: place, - completion: completion + dateTime: dateTime, + place: place ) } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift index b03b5ef..52fa48b 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift @@ -45,23 +45,23 @@ final class AppointmentCreationViewModel { isCreating = true if appointmentTitle.isEmpty { appointmentTitle = "약속" } - createAppointmentUseCase.execute( - title: appointmentTitle, - date: appointmentDate, - place: placeDomain - ) { [weak self] result in + // TODO: 약속 날짜 필수화 때 appointmentDate를 필수로 바꾸고 기본값을 없앤다 + let dateTime = appointmentDate ?? Date() + Task { @MainActor [weak self] in guard let self else { return } - DispatchQueue.main.async { + do { + let appointment = try await self.createAppointmentUseCase.execute( + name: self.appointmentTitle, + dateTime: dateTime, + place: self.placeDomain + ) + self.isCreating = false + self.id = appointment.id + self.code = appointment.code + self.hasCreated = true + } catch { self.isCreating = false - switch result { - case .success(let appointment): - self.id = appointment.id - self.code = appointment.code - self.hasCreated = true - case .failure: - // TODO: 서버 작업 후 실패 케이스 정리해 사용자에게 표시할 실패 텍스트 추가 예정 - break - } + // TODO: 서버 작업 후 실패 케이스 정리해 사용자에게 표시할 실패 텍스트 추가 예정 } } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoViewModel.swift index be8d2e1..3638984 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoViewModel.swift @@ -27,12 +27,11 @@ final class AppointmentInfoViewModel { } func fetchAppointmentInfo() { - fetchAppointmentInfoUseCase.execute(appointmentID: appointmentID) { [weak self] result in - DispatchQueue.main.async { - if case .success(let appointment) = result { - self?.appointmentInfo = AppointmentInfo(appointment: appointment) - } - } + Task { @MainActor [weak self] in + guard let self, + let appointment = try? await self.fetchAppointmentInfoUseCase.execute(appointmentID: self.appointmentID) + else { return } + self.appointmentInfo = AppointmentInfo(appointment: appointment) } } @@ -50,17 +49,16 @@ final class AppointmentInfoViewModel { } private func update(name: String?, date: Date?, place: Place?) { - updateAppointmentInfoUseCase.execute( - appointmentID: appointmentID, - name: name, - date: date, - place: place - ) { [weak self] result in - DispatchQueue.main.async { - if case .success(let appointment) = result { - self?.appointmentInfo = AppointmentInfo(appointment: appointment) - } - } + Task { @MainActor [weak self] in + guard let self, + let appointment = try? await self.updateAppointmentInfoUseCase.execute( + appointmentID: self.appointmentID, + name: name, + dateTime: date, + place: place + ) + else { return } + self.appointmentInfo = AppointmentInfo(appointment: appointment) } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Home/HomeViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Home/HomeViewModel.swift index 22b5f3c..690c09e 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Home/HomeViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Home/HomeViewModel.swift @@ -19,11 +19,11 @@ final class HomeViewModel { } func joinAppointment(code: String, completion: @escaping (Result) -> Void) { - joinAppointmentUseCase.execute(code: code) { result in - switch result { - case .success(let (appointment, _)): + Task { @MainActor in + do { + let appointment = try await joinAppointmentUseCase.execute(code: code) completion(.success(AppointmentInfo(appointment: appointment))) - case .failure(let error): + } catch { completion(.failure(error)) } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteViewModel.swift index c89fe36..2cd3923 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteViewModel.swift @@ -37,16 +37,15 @@ final class AppointmentRouteViewModel { func fetchAppointmentDetail() { isLoading = true - getAppointmentDetailUseCase.execute(appointmentID: appointmentID) { [weak self] result in + Task { @MainActor [weak self] in guard let self else { return } - DispatchQueue.main.async { + do { + let detail = try await self.getAppointmentDetailUseCase.execute(appointmentID: self.appointmentID) self.isLoading = false - switch result { - case .success(let value): - self.apply(appointment: value.appointment, currentUserID: value.currentUserID) - case .failure: - self.errorMessage = "약속 정보를 불러오지 못했습니다" - } + self.apply(appointment: detail.appointment, routes: detail.routes, currentUserID: detail.currentUserID) + } catch { + self.isLoading = false + self.errorMessage = "약속 정보를 불러오지 못했습니다" } } } @@ -55,7 +54,7 @@ final class AppointmentRouteViewModel { onChangeRouteTap?(place) } - private func apply(appointment: Appointment, currentUserID: String) { + private func apply(appointment: Appointment, routes: [String: Route], currentUserID: String) { place = appointment.place placeName = appointment.place?.name placeCoordinate = appointment.place?.coordinate @@ -65,11 +64,11 @@ final class AppointmentRouteViewModel { .map { [$0] + otherParticipants } ?? otherParticipants participants = orderedParticipants.compactMap { user in - guard let route = appointment.routes[user.id] else { return nil } + guard let route = routes[user.id] else { return nil } return AppointmentRouteParticipant(user: user, route: route, currentUserID: currentUserID) } - if let myRoute = appointment.routes[currentUserID] { + if let myRoute = routes[currentUserID] { myRouteSummary = MyRouteSummary(route: myRoute) } } From 28d0876d036f629c977a858bb8f49f5202ef351e Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Mon, 12 Oct 2026 19:44:14 +0900 Subject: [PATCH 3/4] =?UTF-8?q?FEAT:=20=EC=95=BD=EC=86=8D=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=99=94=EB=A9=B4=EC=9D=98=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9D=84=20=EB=8B=A4=EC=9D=8C=20?= =?UTF-8?q?=EC=A0=95=EC=8B=9C=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppointmentCreationViewController.swift | 3 +-- .../AppointmentCreationViewModel.swift | 13 +++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift index f15e879..dadd3c9 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewController.swift @@ -84,8 +84,7 @@ final class AppointmentCreationViewController: UIViewController { viewModel.$appointmentDate .receive(on: DispatchQueue.main) .sink { [weak self] date in - guard let self, let date else { return } - self.creationCard.setDateLabel(date) + self?.creationCard.setDateLabel(date) } .store(in: &cancellables) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift index 52fa48b..e2ffe27 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/AppointmentCreationViewModel.swift @@ -18,7 +18,8 @@ final class AppointmentCreationViewModel { private var isCreating = false private var placeDomain: Place? - @Published private(set) var appointmentDate: Date? + /// 날짜는 필수라 다음 정시로 미리 채워 둔다 + @Published private(set) var appointmentDate: Date = AppointmentCreationViewModel.nextHour() @Published private(set) var place: PlaceInfo? @Published private(set) var hasCreated: Bool = false @@ -45,14 +46,12 @@ final class AppointmentCreationViewModel { isCreating = true if appointmentTitle.isEmpty { appointmentTitle = "약속" } - // TODO: 약속 날짜 필수화 때 appointmentDate를 필수로 바꾸고 기본값을 없앤다 - let dateTime = appointmentDate ?? Date() Task { @MainActor [weak self] in guard let self else { return } do { let appointment = try await self.createAppointmentUseCase.execute( name: self.appointmentTitle, - dateTime: dateTime, + dateTime: self.appointmentDate, place: self.placeDomain ) self.isCreating = false @@ -77,4 +76,10 @@ final class AppointmentCreationViewModel { ) } + private static func nextHour(after date: Date = Date()) -> Date { + let calendar = Calendar.current + let startOfHour = calendar.dateInterval(of: .hour, for: date)?.start ?? date + return calendar.date(byAdding: .hour, value: 1, to: startOfHour) ?? date + } + } From f52eb6986717986141f1cd76852579c486de5071 Mon Sep 17 00:00:00 2001 From: Sang Yu Lee Date: Mon, 12 Oct 2026 19:44:14 +0900 Subject: [PATCH 4/4] =?UTF-8?q?REFACTOR:=20=EC=95=BD=EC=86=8D=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EB=AA=A8=EB=8D=B8=EC=9D=98=20=EB=82=A0=EC=A7=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=95=84=EC=88=98=EA=B0=92=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SubComponent/AppointmentConfirmCard.swift | 2 +- .../AppointmentList/AppointmentListCard.swift | 2 +- .../AppointmentListViewController.swift | 2 +- .../AppointmentListViewModel.swift | 16 ++++++++-------- .../PastAppointmentListViewModel.swift | 4 ++-- .../AppointmentInfo/AppointmentInfoCard.swift | 2 +- .../Presentation/Home/HomeViewModel.swift | 8 ++++---- .../Home/UpcomingAppointmentCard.swift | 4 ++-- .../Detail/AppointmentNotificationCard.swift | 2 +- .../Presentation/MyPage/MyPageViewModel.swift | 6 +++--- .../PresentationModel/AppointmentInfo.swift | 4 ++-- .../PresentationModel/AppointmentListItem.swift | 2 +- .../PresentationModel/UpcomingAppointment.swift | 2 +- 13 files changed, 28 insertions(+), 28 deletions(-) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentConfirmCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentConfirmCard.swift index d9c2693..2efa977 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentConfirmCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentCreation/SubComponent/AppointmentConfirmCard.swift @@ -84,7 +84,7 @@ final class AppointmentConfirmCard: UIView { private func configure(with data: AppointmentInfo) { nameRow.text = data.title - dateRow.text = data.date?.koreanDateString ?? "미정" + dateRow.text = data.date.koreanDateString placeRow.text = data.location?.title ?? "미정" codeLabel.text = "약속 코드 : \(data.code)" } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift index f42646f..b39c9ab 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListCard.swift @@ -59,7 +59,7 @@ final class AppointmentListCard: AppointmentCardBase { super.init( participantCount: item.participantCount, placeText: item.location?.title ?? "미정", - dateText: item.date?.appointmentDateTimeText ?? "미정", + dateText: item.date.appointmentDateTimeText, title: item.title, accessoryView: accessoryView ) diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift index a79309f..cf51870 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewController.swift @@ -204,7 +204,7 @@ final class AppointmentListViewController: UIViewController { let label = UILabel() label.font = .boldPreferredFont(forTextStyle: .footnote) label.textColor = .blue2 - label.text = item.date?.remainingTimeText + label.text = item.date.remainingTimeText return label }() diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift index fea8728..a3ed5c3 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/AppointmentListViewModel.swift @@ -70,7 +70,7 @@ final class AppointmentListViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date()), + date: Calendar.current.startOfDay(for: Date()).addingTimeInterval(12 * 60 * 60), isNotificationEnabled: true ), AppointmentListItem( @@ -82,7 +82,7 @@ final class AppointmentListViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date()), + date: Calendar.current.startOfDay(for: Date()), isNotificationEnabled: true ), AppointmentListItem( @@ -94,7 +94,7 @@ final class AppointmentListViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .minute, value: 30, to: Date()), + date: Date().addingTimeInterval(30 * 60), isNotificationEnabled: true ), AppointmentListItem( @@ -106,7 +106,7 @@ final class AppointmentListViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .day, value: 1, to: Date()), + date: Date().addingTimeInterval(1 * 24 * 60 * 60), isNotificationEnabled: true ), AppointmentListItem( @@ -118,21 +118,21 @@ final class AppointmentListViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .day, value: 365, to: Date()), + date: Date().addingTimeInterval(365 * 24 * 60 * 60), isNotificationEnabled: false ) ] let (today, upcoming) = allAppointments.reduce(into: ([AppointmentListItem](), [AppointmentListItem]())) { result, item in - if let date = item.date, Calendar.current.isDateInToday(date) { + if Calendar.current.isDateInToday(item.date) { result.0.append(item) } else { result.1.append(item) } } - todayAppointments = today.sorted { ($0.date ?? .distantFuture) < ($1.date ?? .distantFuture) } - upcomingAppointments = upcoming.sorted { ($0.date ?? .distantFuture) < ($1.date ?? .distantFuture) } + todayAppointments = today.sorted { $0.date < $1.date } + upcomingAppointments = upcoming.sorted { $0.date < $1.date } } } diff --git a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift index ba0cc01..5d3dc1b 100644 --- a/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/AppointmentList/PastAppointmentListViewModel.swift @@ -62,13 +62,13 @@ final class PastAppointmentListViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .day, value: -index, to: Date()), + date: Date().addingTimeInterval(-Double(index) * 24 * 60 * 60), isNotificationEnabled: true ) } pastAppointments = allPastAppointments.sorted { - ($0.date ?? .distantPast) > ($1.date ?? .distantPast) + $0.date > $1.date } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoCard.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoCard.swift index 2794bf0..3a24eba 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/AppointmentInfoCard.swift @@ -121,7 +121,7 @@ final class AppointmentInfoCard: UIView { /// 변경 가능한 정보(이름, 날짜, 장소, 참여자)를 갱신한다. 매 emit마다 호출. func updateInfo(_ data: AppointmentInfo) { fieldsBox.name = data.title - fieldsBox.dateText = data.date?.appointmentDateTimeText ?? "미정" + fieldsBox.dateText = data.date.appointmentDateTimeText fieldsBox.placeText = data.location?.title ?? "미정" memberSectionLabel.text = "인원 (\(data.participants.count)명)" diff --git a/WhereAreYou/WhereAreYou/Presentation/Home/HomeViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Home/HomeViewModel.swift index 690c09e..90ffab2 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Home/HomeViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Home/HomeViewModel.swift @@ -40,7 +40,7 @@ final class HomeViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .hour, value: 10, to: Date()) + date: Date().addingTimeInterval(10 * 60 * 60) ), UpcomingAppointment( id: "2", @@ -51,7 +51,7 @@ final class HomeViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .hour, value: 30, to: Date()) + date: Date().addingTimeInterval(30 * 60 * 60) ), UpcomingAppointment( id: "3", @@ -62,10 +62,10 @@ final class HomeViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .hour, value: 3, to: Date()) + date: Date().addingTimeInterval(3 * 60 * 60) ) ] - .sorted { ($0.date ?? .distantFuture) < ($1.date ?? .distantFuture) } + .sorted { $0.date < $1.date } } } diff --git a/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift b/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift index f8bf36b..ac752ba 100644 --- a/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/Home/UpcomingAppointmentCard.swift @@ -23,14 +23,14 @@ final class UpcomingAppointmentCard: AppointmentCardBase { label.adjustsFontForContentSizeCategory = true label.setContentHuggingPriority(.required, for: .horizontal) label.setContentCompressionResistancePriority(.required, for: .horizontal) - label.text = appointment.date?.remainingTimeText + label.text = appointment.date.remainingTimeText return label }() super.init( participantCount: appointment.participantCount, placeText: appointment.location?.title ?? "미정", - dateText: appointment.date?.appointmentDateTimeText ?? "미정", + dateText: appointment.date.appointmentDateTimeText, title: appointment.title, accessoryView: remainingTimeLabel ) diff --git a/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/AppointmentNotificationCard.swift b/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/AppointmentNotificationCard.swift index 0d32903..3eaf219 100644 --- a/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/AppointmentNotificationCard.swift +++ b/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/AppointmentNotificationCard.swift @@ -26,7 +26,7 @@ final class AppointmentNotificationCard: AppointmentCardBase { super.init( participantCount: item.participantCount, placeText: item.location?.title ?? "미정", - dateText: item.date?.appointmentDateTimeText ?? "미정", + dateText: item.date.appointmentDateTimeText, title: item.title, accessoryView: notificationSwitch ) diff --git a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift index ec410a7..bda9850 100644 --- a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift @@ -130,7 +130,7 @@ extension MyPageViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .day, value: 1, to: Date()), + date: Date().addingTimeInterval(1 * 24 * 60 * 60), isNotificationEnabled: true ), AppointmentListItem( @@ -142,7 +142,7 @@ extension MyPageViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .day, value: 3, to: Date()), + date: Date().addingTimeInterval(3 * 24 * 60 * 60), isNotificationEnabled: false ), AppointmentListItem( @@ -154,7 +154,7 @@ extension MyPageViewModel { address: "", coordinate: Coordinate(latitude: 0, longitude: 0) ), - date: Calendar.current.date(byAdding: .day, value: 10, to: Date()), + date: Date().addingTimeInterval(10 * 24 * 60 * 60), isNotificationEnabled: true ) ] diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentInfo.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentInfo.swift index 510b598..d597632 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentInfo.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentInfo.swift @@ -12,7 +12,7 @@ struct AppointmentInfo { let id: String let code: String let title: String - let date: Date? + let date: Date let location: AppointmentLocation? let participants: [Participant] @@ -30,7 +30,7 @@ struct AppointmentInfo { id: String, code: String, title: String, - date: Date?, + date: Date, location: AppointmentLocation?, participants: [Participant] ) { diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentListItem.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentListItem.swift index 1913209..3db03d0 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentListItem.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentListItem.swift @@ -14,6 +14,6 @@ struct AppointmentListItem { let title: String let participantCount: Int let location: AppointmentLocation? - let date: Date? + let date: Date let isNotificationEnabled: Bool } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/UpcomingAppointment.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/UpcomingAppointment.swift index 51821d2..4f704ca 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/UpcomingAppointment.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/UpcomingAppointment.swift @@ -12,5 +12,5 @@ struct UpcomingAppointment { let title: String let participantCount: Int let location: AppointmentLocation? - let date: Date? + let date: Date }