Repository navigation
클래스 이용권 확인 및 재등록 경로 연결 #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -90,6 +90,7 @@ public struct ClassCoordinator { | |||||
|
|
||||||
| public enum AsyncAction: Equatable {} | ||||||
| public enum InnerAction: Equatable { | ||||||
| case ticketRegistrationCompleted(routeID: Int, instanceID: UUID, startDate: Date, endDate: Date) | ||||||
| case membersLoaded( | ||||||
| routeID: Int, | ||||||
| roomID: Int, | ||||||
|
|
@@ -479,6 +480,51 @@ private extension ClassCoordinator { | |||||
| state.routes.push(.ticket(.init(instanceID: uuid()))) | ||||||
| return .none | ||||||
|
|
||||||
| case let .routeAction(routeID, action: .ticket(.delegate(.showOverview(startDate, endDate)))): | ||||||
| guard routeID == state.routes.count - 1, | ||||||
| case let .ticket(completed)? = state.routes.last?.screen, | ||||||
| completed.phase == .registered, | ||||||
| completed.registrationStartDate == startDate, | ||||||
| completed.expirationDate == endDate | ||||||
| else { return .none } | ||||||
| return .send(.inner(.ticketRegistrationCompleted( | ||||||
| routeID: routeID, | ||||||
| instanceID: completed.instanceID, | ||||||
| startDate: startDate, | ||||||
| endDate: endDate | ||||||
| ))) | ||||||
|
|
||||||
| case let .routeAction(routeID, action: .ticket(.delegate(.registered(startDate, endDate)))): | ||||||
| guard routeID == state.routes.count - 1, | ||||||
| routeID >= 2, | ||||||
| case let .ticket(registered)? = state.routes.last?.screen, | ||||||
| registered.phase == .registered, | ||||||
| registered.registrationStartDate == startDate, | ||||||
| registered.expirationDate == endDate, | ||||||
| case var .ticket(overview) = state.routes[routeID - 1].screen, | ||||||
| overview.phase == .overview | ||||||
| else { return .none } | ||||||
| overview.registrationStartDate = startDate | ||||||
| overview.expirationDate = endDate | ||||||
| state.routes[screenAt: routeID - 1] = .ticket(overview) | ||||||
| if routeID >= 3, | ||||||
| case var .ticket(original) = state.routes[routeID - 2].screen, | ||||||
| original.phase == .registered | ||||||
| { | ||||||
| original.registrationStartDate = startDate | ||||||
| original.expirationDate = endDate | ||||||
| state.routes[screenAt: routeID - 2] = .ticket(original) | ||||||
| } | ||||||
| return .none | ||||||
|
|
||||||
| case let .routeAction(routeID, action: .ticket(.delegate(.addRegistration))): | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P3] Minor Delegate 액션의 네이밍 컨벤션에 따라 액션을 수정해야 합니다. 관련 수정 제안은
Suggested change
|
||||||
| guard routeID == state.routes.count - 1, | ||||||
| case let .ticket(overview)? = state.routes.last?.screen, | ||||||
| overview.phase == .overview | ||||||
| else { return .none } | ||||||
| state.routes.push(.ticket(.init(instanceID: uuid()))) | ||||||
| return .none | ||||||
|
|
||||||
| case let .routeAction(routeID, action: .ticket(.delegate(.dismiss))): | ||||||
| guard routeID == state.routes.count - 1, | ||||||
| case .ticket = state.routes.last?.screen | ||||||
|
|
@@ -1028,6 +1074,42 @@ private extension ClassCoordinator { | |||||
| action: InnerAction | ||||||
| ) -> Effect<Action> { | ||||||
| switch action { | ||||||
| case let .ticketRegistrationCompleted(routeID, instanceID, startDate, endDate): | ||||||
| guard routeID == state.routes.count - 1, | ||||||
| case let .ticket(completed)? = state.routes.last?.screen, | ||||||
| completed.instanceID == instanceID, | ||||||
| completed.phase == .registered, | ||||||
| completed.registrationStartDate == startDate, | ||||||
| completed.expirationDate == endDate | ||||||
| else { return .none } | ||||||
|
|
||||||
| if routeID >= 2, | ||||||
| case let .ticket(previous) = state.routes[routeID - 1].screen, | ||||||
| previous.phase == .overview | ||||||
| { | ||||||
| state.routes.goBack() | ||||||
| var overview = previous | ||||||
| overview.registrationStartDate = startDate | ||||||
| overview.expirationDate = endDate | ||||||
| state.routes[screenAt: routeID - 1] = .ticket(overview) | ||||||
| if routeID >= 3, | ||||||
| case var .ticket(original) = state.routes[routeID - 2].screen, | ||||||
| original.phase == .registered | ||||||
| { | ||||||
| original.registrationStartDate = startDate | ||||||
| original.expirationDate = endDate | ||||||
| state.routes[screenAt: routeID - 2] = .ticket(original) | ||||||
| } | ||||||
| return .none | ||||||
| } | ||||||
|
|
||||||
| var overview = ClassTicketFeature.State(instanceID: uuid()) | ||||||
| overview.phase = .overview | ||||||
| overview.registrationStartDate = startDate | ||||||
| overview.expirationDate = endDate | ||||||
| state.routes.push(.ticket(overview)) | ||||||
| return .none | ||||||
|
|
||||||
| case let .joinedSessionCleared(requestID, room): | ||||||
| guard state.pendingJoinID == requestID, | ||||||
| isCompletingJoinPrefix(state) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1919,6 +1919,114 @@ struct ClassParticipantPushTests { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| @Test | ||||||
| func 이용권_확인은_push하고_재등록은_기존_확인_경로를_갱신한다() async throws { | ||||||
| let firstStart = try #require("2026.10.09".date(as: .yearMonthDayDotted)) | ||||||
| let firstEnd = try #require("2027.04.08".date(as: .yearMonthDayDotted)) | ||||||
| let secondStart = try #require("2026.11.01".date(as: .yearMonthDayDotted)) | ||||||
| let secondEnd = try #require("2027.04.30".date(as: .yearMonthDayDotted)) | ||||||
| let clock = TestClock() | ||||||
| let originalID = UUID(400) | ||||||
| var registration = ClassTicketFeature.State(instanceID: originalID) | ||||||
| registration.phase = .registered | ||||||
| registration.registrationStartDate = firstStart | ||||||
| registration.expirationDate = firstEnd | ||||||
| var state = ClassCoordinator.State() | ||||||
| state.routes.push(.ticket(registration)) | ||||||
| state.ticketInstanceIDs = [originalID] | ||||||
| let store = TestStore(initialState: state) { ClassCoordinator() } withDependencies: { | ||||||
| $0.uuid = .incrementing | ||||||
| $0.continuousClock = clock | ||||||
| $0.date = .constant(secondStart) | ||||||
| } | ||||||
|
|
||||||
| await store.send(.router(.routeAction( | ||||||
| id: 1, | ||||||
| action: .ticket(.delegate(.showOverview(firstStart, firstEnd))) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P3] Minor Delegate 액션의 네이밍 컨벤션에 따라 액션을 수정해야 합니다. 관련 수정 제안은
Suggested change
|
||||||
| ))) | ||||||
| await store.receive(\.inner) { | ||||||
| var overview = ClassTicketFeature.State(instanceID: UUID(0)) | ||||||
| overview.phase = .overview | ||||||
| overview.registrationStartDate = firstStart | ||||||
| overview.expirationDate = firstEnd | ||||||
| $0.routes.push(.ticket(overview)) | ||||||
| $0.ticketInstanceIDs = [originalID, UUID(0)] | ||||||
| } | ||||||
| #expect(store.state.showsTabBar) | ||||||
|
|
||||||
| await store.send(.router(.routeAction( | ||||||
| id: 2, | ||||||
| action: .ticket(.delegate(.addRegistration)) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P3] Minor Delegate 액션의 네이밍 컨벤션에 따라 액션을 수정해야 합니다. 관련 수정 제안은
Suggested change
|
||||||
| ))) { | ||||||
| $0.routes.push(.ticket(.init(instanceID: UUID(1)))) | ||||||
| $0.ticketInstanceIDs = [originalID, UUID(0), UUID(1)] | ||||||
| } | ||||||
| await store.send(.router(.routeAction( | ||||||
| id: 3, | ||||||
| action: .ticket(.binding(.set(\.code, "PICKE30"))) | ||||||
| ))) { | ||||||
| guard case var .ticket(ticket) = $0.routes[3].screen else { return } | ||||||
| ticket.code = "PICKE30" | ||||||
| $0.routes[screenAt: 3] = .ticket(ticket) | ||||||
| } | ||||||
| await store.send(.router(.routeAction( | ||||||
| id: 3, | ||||||
| action: .ticket(.view(.checkTapped)) | ||||||
| ))) { | ||||||
| guard case var .ticket(ticket) = $0.routes[3].screen else { return } | ||||||
| ticket.isChecking = true | ||||||
| $0.routes[screenAt: 3] = .ticket(ticket) | ||||||
| } | ||||||
| await clock.advance(by: .milliseconds(350)) | ||||||
| await store.receive(\.router.routeAction) { | ||||||
| guard case var .ticket(ticket) = $0.routes[3].screen else { return } | ||||||
| ticket.isChecking = false | ||||||
| ticket.phase = .registered | ||||||
| ticket.registrationStartDate = secondStart | ||||||
| ticket.expirationDate = secondEnd | ||||||
| $0.routes[screenAt: 3] = .ticket(ticket) | ||||||
| } | ||||||
| await store.receive(\.router.routeAction) { | ||||||
| guard case var .ticket(overview) = $0.routes[2].screen, | ||||||
| case var .ticket(original) = $0.routes[1].screen | ||||||
| else { return } | ||||||
| overview.registrationStartDate = secondStart | ||||||
| overview.expirationDate = secondEnd | ||||||
| original.registrationStartDate = secondStart | ||||||
| original.expirationDate = secondEnd | ||||||
| $0.routes[screenAt: 2] = .ticket(overview) | ||||||
| $0.routes[screenAt: 1] = .ticket(original) | ||||||
| } | ||||||
| await store.send(.router(.routeAction( | ||||||
| id: 3, | ||||||
| action: .ticket(.delegate(.showOverview(secondStart, secondEnd))) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P3] Minor Delegate 액션의 네이밍 컨벤션에 따라 액션을 수정해야 합니다. 관련 수정 제안은
Suggested change
|
||||||
| ))) | ||||||
| #expect(store.state.routes.count == 4) | ||||||
| await store.receive(\.inner) { | ||||||
| $0.routes.goBack() | ||||||
| guard case var .ticket(overview) = $0.routes[2].screen, | ||||||
| case var .ticket(original) = $0.routes[1].screen | ||||||
| else { return } | ||||||
| overview.registrationStartDate = secondStart | ||||||
| overview.expirationDate = secondEnd | ||||||
| original.registrationStartDate = secondStart | ||||||
| original.expirationDate = secondEnd | ||||||
| $0.routes[screenAt: 2] = .ticket(overview) | ||||||
| $0.routes[screenAt: 1] = .ticket(original) | ||||||
| $0.ticketInstanceIDs = [originalID, UUID(0)] | ||||||
| } | ||||||
| #expect(store.state.routes.count == 3) | ||||||
| await store.send(.view(.backAction)) { | ||||||
| $0.routes.goBack() | ||||||
| $0.ticketInstanceIDs = [originalID] | ||||||
| } | ||||||
| guard case let .ticket(original) = store.state.routes.last?.screen else { | ||||||
| Issue.record("뒤로 이동 후 등록 화면이 사라졌습니다") | ||||||
| return | ||||||
| } | ||||||
| #expect(original.periodText == "2026.11.01 – 2027.04.30") | ||||||
| } | ||||||
|
|
||||||
| @Test | ||||||
| func 이용권_확인_중_스와이프로_경로가_제거되면_응답도_취소된다() async { | ||||||
| let clock = TestClock() | ||||||
|
|
@@ -1946,6 +2054,94 @@ struct ClassParticipantPushTests { | |||||
| await store.finish(timeout: .seconds(1)) | ||||||
| } | ||||||
|
|
||||||
| @Test | ||||||
| func 추가등록_성공_후_뒤로가도_확인_화면에_새_기간이_남는다() async throws { | ||||||
| let oldStart = try #require("2026.10.09".date(as: .yearMonthDayDotted)) | ||||||
| let oldEnd = try #require("2027.04.08".date(as: .yearMonthDayDotted)) | ||||||
| let newStart = try #require("2026.11.01".date(as: .yearMonthDayDotted)) | ||||||
| let newEnd = try #require("2027.04.30".date(as: .yearMonthDayDotted)) | ||||||
| var original = ClassTicketFeature.State(instanceID: UUID(400)) | ||||||
| original.phase = .registered | ||||||
| original.registrationStartDate = oldStart | ||||||
| original.expirationDate = oldEnd | ||||||
| var overview = ClassTicketFeature.State(instanceID: UUID(401)) | ||||||
| overview.phase = .overview | ||||||
| overview.registrationStartDate = oldStart | ||||||
| overview.expirationDate = oldEnd | ||||||
| var registration = ClassTicketFeature.State(instanceID: UUID(402)) | ||||||
| registration.phase = .registered | ||||||
| registration.registrationStartDate = newStart | ||||||
| registration.expirationDate = newEnd | ||||||
| var state = ClassCoordinator.State() | ||||||
| state.routes.push(.ticket(original)) | ||||||
| state.routes.push(.ticket(overview)) | ||||||
| state.routes.push(.ticket(registration)) | ||||||
| state.ticketInstanceIDs = [original.instanceID, overview.instanceID, registration.instanceID] | ||||||
| let store = TestStore(initialState: state) { ClassCoordinator() } | ||||||
|
|
||||||
| await store.send(.router(.routeAction( | ||||||
| id: 3, | ||||||
| action: .ticket(.delegate(.registered(newStart, newEnd))) | ||||||
| ))) { | ||||||
| guard case var .ticket(currentOverview) = $0.routes[2].screen, | ||||||
| case var .ticket(currentOriginal) = $0.routes[1].screen | ||||||
| else { return } | ||||||
| currentOverview.registrationStartDate = newStart | ||||||
| currentOverview.expirationDate = newEnd | ||||||
| currentOriginal.registrationStartDate = newStart | ||||||
| currentOriginal.expirationDate = newEnd | ||||||
| $0.routes[screenAt: 2] = .ticket(currentOverview) | ||||||
| $0.routes[screenAt: 1] = .ticket(currentOriginal) | ||||||
| } | ||||||
| await store.send(.view(.backAction)) { | ||||||
| $0.routes.goBack() | ||||||
| $0.ticketInstanceIDs = [original.instanceID, overview.instanceID] | ||||||
| } | ||||||
| guard case let .ticket(currentOverview) = store.state.routes.last?.screen else { | ||||||
| Issue.record("추가등록 뒤 확인 화면이 사라졌습니다") | ||||||
| return | ||||||
| } | ||||||
| #expect(currentOverview.periodText == "2026.11.01 – 2027.04.30") | ||||||
| } | ||||||
|
|
||||||
| @Test | ||||||
| func 추가등록_조회_중_경로가_제거되면_응답을_취소한다() async throws { | ||||||
| let start = try #require("2026.10.09".date(as: .yearMonthDayDotted)) | ||||||
| let end = try #require("2027.04.08".date(as: .yearMonthDayDotted)) | ||||||
| let clock = TestClock() | ||||||
| var original = ClassTicketFeature.State(instanceID: UUID(400)) | ||||||
| original.phase = .registered | ||||||
| original.registrationStartDate = start | ||||||
| original.expirationDate = end | ||||||
| var overview = ClassTicketFeature.State(instanceID: UUID(401)) | ||||||
| overview.phase = .overview | ||||||
| overview.registrationStartDate = start | ||||||
| overview.expirationDate = end | ||||||
| var registration = ClassTicketFeature.State(instanceID: UUID(402)) | ||||||
| registration.code = "PICKE30" | ||||||
| var state = ClassCoordinator.State() | ||||||
| state.routes.push(.ticket(original)) | ||||||
| state.routes.push(.ticket(overview)) | ||||||
| state.routes.push(.ticket(registration)) | ||||||
| state.ticketInstanceIDs = [original.instanceID, overview.instanceID, registration.instanceID] | ||||||
| let store = TestStore(initialState: state) { ClassCoordinator() } withDependencies: { | ||||||
| $0.continuousClock = clock | ||||||
| $0.date = .constant(start) | ||||||
| } | ||||||
|
|
||||||
| await store.send(.router(.routeAction(id: 3, action: .ticket(.view(.checkTapped))))) { | ||||||
| guard case var .ticket(current) = $0.routes[3].screen else { return } | ||||||
| current.isChecking = true | ||||||
| $0.routes[screenAt: 3] = .ticket(current) | ||||||
| } | ||||||
| await store.send(.router(.updateRoutes(Array(state.routes.dropLast())))) { | ||||||
| $0.routes = Array($0.routes.dropLast()) | ||||||
| $0.ticketInstanceIDs = [original.instanceID, overview.instanceID] | ||||||
| } | ||||||
| await clock.advance(by: .milliseconds(350)) | ||||||
| await store.finish(timeout: .seconds(1)) | ||||||
| } | ||||||
|
|
||||||
| @Test | ||||||
| func 인트로_참여는_코드_화면을_한번만_push한다() async { | ||||||
| let codeID = UUID(201) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,4 +3,5 @@ import Foundation | |
| public enum ClassTicketPhase: Equatable, Sendable { | ||
| case entry | ||
| case registered | ||
| case overview | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -30,6 +30,10 @@ public struct ClassTicketFeature { | |||||
| !code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isChecking | ||||||
| } | ||||||
|
|
||||||
| public var navigationTitle: String { | ||||||
| phase == .overview ? "클래스 이용권" : "이용권 등록하기" | ||||||
| } | ||||||
|
|
||||||
| public var periodText: String { | ||||||
| guard let registrationStartDate, let expirationDate else { return "" } | ||||||
| return "\(registrationStartDate.formatted(.yearMonthDayDotted)) – \(expirationDate.formatted(.yearMonthDayDotted))" | ||||||
|
|
@@ -48,12 +52,16 @@ public struct ClassTicketFeature { | |||||
| case backTapped | ||||||
| case checkTapped | ||||||
| case confirmTapped | ||||||
| case addRegistrationTapped | ||||||
| case faqTapped(Int) | ||||||
| } | ||||||
|
|
||||||
| @CasePathable | ||||||
| public enum DelegateAction: Equatable { | ||||||
| case dismiss | ||||||
| case showOverview(Date, Date) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P3] Minor Delegate 액션은 자식 Reducer에서 발생한 이벤트를 명확히 설명해야 합니다.
Suggested change
|
||||||
| case addRegistration | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P3] Minor Delegate 액션은 자식 Reducer에서 발생한 이벤트를 명확히 설명해야 합니다.
Suggested change
|
||||||
| case registered(Date, Date) | ||||||
| } | ||||||
|
|
||||||
| public enum CancelID: Hashable, Sendable { | ||||||
|
|
@@ -76,9 +84,21 @@ public struct ClassTicketFeature { | |||||
| case .binding: | ||||||
| return .none | ||||||
|
|
||||||
| case .view(.backTapped), .view(.confirmTapped): | ||||||
| case .view(.backTapped): | ||||||
| return .send(.delegate(.dismiss)) | ||||||
|
|
||||||
| case .view(.confirmTapped): | ||||||
| guard | ||||||
| state.phase == .registered, | ||||||
| let registrationStartDate = state.registrationStartDate, | ||||||
| let expirationDate = state.expirationDate | ||||||
| else { return .none } | ||||||
| return .send(.delegate(.showOverview(registrationStartDate, expirationDate))) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P3] Minor Delegate 액션의 네이밍 컨벤션에 따라 액션을 수정해야 합니다. 관련 수정 제안은 LINE 62를 참조하세요.
Suggested change
|
||||||
|
|
||||||
| case .view(.addRegistrationTapped): | ||||||
| guard state.phase == .overview else { return .none } | ||||||
| return .send(.delegate(.addRegistration)) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 [P3] Minor Delegate 액션의 네이밍 컨벤션에 따라 액션을 수정해야 합니다. 관련 수정 제안은 LINE 63을 참조하세요.
Suggested change
|
||||||
|
|
||||||
| case let .view(.faqTapped(index)): | ||||||
| state.expandedFAQ = state.expandedFAQ == index ? nil : index | ||||||
| return .none | ||||||
|
|
@@ -111,6 +131,7 @@ public struct ClassTicketFeature { | |||||
| state.registrationStartDate = start | ||||||
| state.expirationDate = lastDay | ||||||
| state.phase = .registered | ||||||
| return .send(.delegate(.registered(start, lastDay))) | ||||||
|
|
||||||
| case .failure: | ||||||
| state.errorMessage = "유효하지 않은 이용권 코드예요. 다시 확인해 주세요." | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 [P3] Minor
Delegate 액션의 네이밍 컨벤션에 따라 액션을 수정해야 합니다. 관련 수정 제안은
ClassTicketFeature.swiftLINE 62를 참조하세요.