Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public extension ModulePaths {
case SchoolMajorSheetFeature
case RootFeature
case NoticeFeature
case OnboardingFeature
case ModifyTimeTableFeature
case MealFeature
case MainFeature
Expand Down
13 changes: 12 additions & 1 deletion Projects/App/iOS/Sources/Application/TodayWhatApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,24 @@ struct TodayWhatApp: App {
RootCore()
}
)
)
) {
siriSection
}
.onOpenURL { url in
guard let route = TodayWhatAppRoute.from(url: url) else { return }
TodayWhatAppRouteStore.shared.request(route)
}
}
}

@ViewBuilder
private var siriSection: some View {
if #available(iOS 17, *) {
SiriTipView(intent: GetMealIntent(mealTime: .all, daySelection: .today))
} else if #available(iOS 16, *) {
ShortcutsLink()
}
}
}

extension UINavigationController: ObservableObject, UIGestureRecognizerDelegate {
Expand Down
4 changes: 2 additions & 2 deletions Projects/App/iOS/Support/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>13.0</string>
<string>14.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand All @@ -34,7 +34,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>92</string>
<string>98</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
Expand Down
18 changes: 18 additions & 0 deletions Projects/Feature/OnboardingFeature/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import DependencyPlugin
import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.module(
name: ModulePaths.Feature.OnboardingFeature.rawValue,
targets: [
.implements(module: .feature(.OnboardingFeature), dependencies: [
.feature(target: .BaseFeature),
.feature(target: .SchoolSettingFeature),
.shared(target: .DateUtil),
.shared(target: .Entity),
.shared(target: .MealClient),
.shared(target: .TimeTableClient),
.userInterface(target: .DesignSystem)
])
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import TWLog

struct OnboardingCompleteEventLog: EventLog {
let name: String = "onboarding_complete"
let params: [String: String] = [:]
}
246 changes: 246 additions & 0 deletions Projects/Feature/OnboardingFeature/Sources/OnboardingCore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import BaseFeature
import ComposableArchitecture
import DateUtil
import Entity
import Foundation
import MealClient
import SchoolSettingFeature
import TimeTableClient
import TWLog

public struct OnboardingCore: Reducer {
public init() {}

public enum Step: Int, CaseIterable, Equatable {
case school
case meal
case timetable
case widget
case ecosystem

public var index: Int { rawValue + 1 }
public var totalCount: Int { Self.allCases.count }
}

public struct State: Equatable {
public var step: Step
public var schoolSettingCore: SchoolSettingCore.State
public var schoolName: String
public var meal: Meal?
public var mealUsesFallback: Bool
public var isMealLoading: Bool
public var mealDisplayDate: Date
public var timeTables: [TimeTable]
public var timeTableUsesFallback: Bool
public var isTimeTableLoading: Bool
public var timeTableDisplayDate: Date

public init(
step: Step = .school,
schoolSettingCore: SchoolSettingCore.State = .init(),
schoolName: String = "",
meal: Meal? = nil,
mealUsesFallback: Bool = false,
isMealLoading: Bool = false,
mealDisplayDate: Date = Date(),
timeTables: [TimeTable] = [],
timeTableUsesFallback: Bool = false,
isTimeTableLoading: Bool = false,
timeTableDisplayDate: Date = Date()
) {
self.step = step
self.schoolSettingCore = schoolSettingCore
self.schoolName = schoolName
self.meal = meal
self.mealUsesFallback = mealUsesFallback
self.isMealLoading = isMealLoading
self.mealDisplayDate = mealDisplayDate
self.timeTables = timeTables
self.timeTableUsesFallback = timeTableUsesFallback
self.isTimeTableLoading = isTimeTableLoading
self.timeTableDisplayDate = timeTableDisplayDate
}

public var progressText: String {
"\(step.index) / \(step.totalCount)"
}

public var progressValue: Double {
Double(step.index) / Double(step.totalCount)
}
}

@CasePathable
public enum Action: Equatable {
case onAppear
case schoolSettingCore(SchoolSettingCore.Action)
case backButtonDidTap
case nextButtonDidTap
case fetchPreviewData
case fetchMeal(Date)
case mealResponse(Date, TaskResult<Meal>)
case fetchTimeTable(Date)
case timeTableResponse(Date, TaskResult<[TimeTable]>)
case onboardingFinished
}

@Dependency(\.mealClient) var mealClient
@Dependency(\.timeTableClient) var timeTableClient
Comment on lines +87 to +88
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Reducer 내부에서 Date()를 직접 호출하는 대신 TCA의 date 디펜던시를 사용하는 것이 좋습니다. 이는 비즈니스 로직의 순수성을 유지하고, 테스트 코드 작성 시 현재 시간을 제어할 수 있게 해줍니다.

Suggested change
@Dependency(\.mealClient) var mealClient
@Dependency(\.timeTableClient) var timeTableClient
@Dependency(\.mealClient) var mealClient
@Dependency(\.timeTableClient) var timeTableClient
@Dependency(\.date.now) var now


public var body: some ReducerOf<OnboardingCore> {
Scope(state: \.schoolSettingCore, action: \.schoolSettingCore) {
SchoolSettingCore()
}

Reduce { state, action in
switch action {
case .onAppear:
self.logStepShowed(step: state.step)
return .none

case .schoolSettingCore(.schoolSettingFinished):
state.schoolName = state.schoolSettingCore.school
state.step = .meal
state.isMealLoading = true
state.isTimeTableLoading = true
self.logStepShowed(step: state.step)
return .send(.fetchPreviewData)

case .schoolSettingCore:
return .none

case .backButtonDidTap:
switch state.step {
case .school:
return .none
case .meal:
state.step = .school
case .timetable:
state.step = .meal
case .widget:
state.step = .timetable
case .ecosystem:
state.step = .widget
}
self.logStepShowed(step: state.step)
return .none

case .nextButtonDidTap:
switch state.step {
case .school:
return .none
case .meal:
state.step = .timetable
case .timetable:
state.step = .widget
case .widget:
state.step = .ecosystem
case .ecosystem:
return .send(.onboardingFinished)
}
self.logStepShowed(step: state.step)
return .none


case .fetchPreviewData:
let targetDate = schoolDay(from: Date())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

앞서 추가한 now 디펜던시를 사용하여 현재 날짜를 가져오도록 수정합니다.

Suggested change
let targetDate = schoolDay(from: Date())
let targetDate = schoolDay(from: now)

state.mealDisplayDate = targetDate
state.timeTableDisplayDate = targetDate
return .merge(
.send(.fetchMeal(targetDate)),
.send(.fetchTimeTable(targetDate))
)

case let .fetchMeal(targetDate):
return .run { send in
await send(
.mealResponse(
targetDate,
TaskResult {
try await mealClient.fetchMeal(targetDate)
}
)
)
}

case let .mealResponse(targetDate, .success(meal)):
state.mealDisplayDate = targetDate
state.meal = meal.isEmpty ? nil : meal
state.mealUsesFallback = meal.isEmpty
state.isMealLoading = false
return .none

case let .mealResponse(targetDate, .failure):
state.mealDisplayDate = targetDate
state.meal = nil
state.mealUsesFallback = true
state.isMealLoading = false
return .none

case let .fetchTimeTable(targetDate):
return .run { send in
await send(
.timeTableResponse(
targetDate,
TaskResult {
try await timeTableClient.fetchTimeTable(targetDate)
}
)
)
}

case let .timeTableResponse(targetDate, .success(timeTables)):
let sorted = timeTables
.filter { !$0.content.isEmpty }
.sorted { $0.perio < $1.perio }
state.timeTableDisplayDate = targetDate
state.timeTables = sorted
state.timeTableUsesFallback = sorted.isEmpty
state.isTimeTableLoading = false
return .none

case let .timeTableResponse(targetDate, .failure):
state.timeTableDisplayDate = targetDate
state.timeTables = []
state.timeTableUsesFallback = true
state.isTimeTableLoading = false
return .none

case .onboardingFinished:
TWLog.event(OnboardingCompleteEventLog())
return .none
}
}
}

private func schoolDay(from date: Date) -> Date {
switch date.weekday {
case 7:
return date.adding(by: .day, value: 2)
case 1:
return date.adding(by: .day, value: 1)
default:
return date
}
}

private func logStepShowed(step: Step) {
let log = PageShowedEventLog(pageName: onboardingPageName(for: step))
TWLog.event(log)
}

private func onboardingPageName(for step: Step) -> String {
switch step {
case .school:
return "onboarding_school_page"
case .meal:
return "onboarding_meal_page"
case .timetable:
return "onboarding_timetable_page"
case .widget:
return "onboarding_widget_page"
case .ecosystem:
return "onboarding_ecosystem_page"
}
}
}
Loading
Loading