Skip to content
Merged
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
11 changes: 10 additions & 1 deletion Projects/App/Sources/Navigation/Profile/ProfileCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation

import AuthDomainInterface
import DDDSharedUI
import OnBoarding
import Profile
Expand All @@ -22,6 +23,7 @@ public struct ProfileCoordinator: Sendable {
@ObservableState
public struct State: Equatable {
var routes: [Route<ProfileScreen.State>]
@Shared(.isEditingGeneration) var editGeneration

public init() {
routes = [.root(.profile(.init()), embedInNavigationView: true)]
Expand Down Expand Up @@ -51,14 +53,20 @@ public struct ProfileCoordinator: Sendable {

}

public enum NavigationAction: Equatable {
public enum NavigationAction: Equatable, Sendable {
case presentLogin
case presentRoot
case presentStaff
case presentMember
}

func handleRoute(state: inout State, action: Action) -> Effect<Action> {
defer {
// ๋ฒ„ํŠผ๋ฟ ์•„๋‹ˆ๋ผ ์‹œ์Šคํ…œ ๋’ค๋กœ๊ฐ€๊ธฐ(updateRoutes)๋กœ ํŽธ์ง‘ ํ™”๋ฉด์ด ๋‹ซํ˜€๋„ ์กฐํšŒ๋ฅผ ์žฌ๊ฐœํ•œ๋‹ค.
if state.editGeneration, !state.routes.contains(where: { $0.screen.onBoarding != nil }) {
state.$editGeneration.withLock { $0 = false }
}
}
switch action {
case .router(let action):
return routerAction(state: &state, action: action)
Expand Down Expand Up @@ -177,6 +185,7 @@ extension ProfileCoordinator {
state: inout State,
action: NavigationAction
) -> Effect<Action> {
state.$editGeneration.withLock { $0 = false }
switch action {
case .presentLogin:
return .none
Expand Down
2 changes: 2 additions & 0 deletions Projects/App/Sources/Reducer/AppReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public struct AppReducer: Sendable {
@Dependency(\.continuousClock) var clock
@Shared(.userSession) var userSession
@Shared(.staffRole) var staffRole
@Shared(.isEditingGeneration) var editGeneration

private enum CancelID: Hashable {
case transition
Expand Down Expand Up @@ -176,6 +177,7 @@ public struct AppReducer: Sendable {
// ๋กœ๊ทธ์•„์›ƒยทํ† ํฐ ๋งŒ๋ฃŒ ์ดํ›„ ์ƒˆ ๊ณ„์ •์˜ ํ”„๋กœํ•„ ์กฐํšŒ ์ „๊นŒ์ง€ ์ด์ „ ID๊ฐ€ ๋‚จ์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.
$userSession.withLock { $0 = .empty }
$staffRole.withLock { $0 = nil }
$editGeneration.withLock { $0 = false }
state = .auth(.init())
return .none

Expand Down
73 changes: 73 additions & 0 deletions Projects/App/Tests/Sources/LogoutNavigationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Testing
@testable import DDDStorage
import AuthDomainInterface
import DDDStorageInterface
@testable import Profile
import ProfileDomainInterface

import ComposableArchitecture
import SQLiteData
Expand Down Expand Up @@ -64,16 +66,19 @@ struct LogoutNavigationTests {
} operation: {
@Shared(.userSession) var session
@Shared(.staffRole) var role
@Shared(.isEditingGeneration) var editing
$session.withLock {
$0 = UserSession(userID: 42, name: "์ด์ „ ๊ณ„์ •", userRole: .manager, accessToken: "old-token")
}
$role.withLock { $0 = .manager }
$editing.withLock { $0 = true }
let store = Store(initialState: AppReducer.State.member(.init())) { AppReducer() }

await store.send(expired ? .async(.refreshTokenExpired) : .view(.presentAuth)).finish()

#expect(session == .empty)
#expect(role == nil)
#expect(!editing)
guard case .auth = store.state else {
Issue.record("์„ธ์…˜์„ ๋น„์šด ๋’ค ์ธ์ฆ ํ™”๋ฉด์œผ๋กœ ์ „ํ™˜ํ•ด์•ผ ํ•œ๋‹ค")
return
Expand All @@ -84,6 +89,74 @@ struct LogoutNavigationTests {
}
}

@Test("๊ธฐ์ˆ˜ ๋ณ€๊ฒฝ์„ ๋ฒ„ํŠผ์ด๋‚˜ ์‹œ์Šคํ…œ ๋’ค๋กœ๊ฐ€๊ธฐ๋กœ ์ทจ์†Œํ•˜๋ฉด ๋‚ด ์ •๋ณด ์žฌ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค", arguments: [false, true])
func generationEditCancellationRestoresProfileLoading(systemBack: Bool) async {
await withDependencies {
$0.defaultInMemoryStorage = InMemoryStorage()
$0.profileUseCase = MockProfileRepository()
} operation: {
let store = Store(initialState: ProfileCoordinator.State()) { ProfileCoordinator() }
await store.send(.router(.routeAction(
id: 0, action: .profile(.delegate(.presentEditGeneration))
))).finish()
#expect(store.editGeneration)
#expect(store.routes.count == 2)

// ํŽธ์ง‘ ๋„์ค‘์˜ ๋‹จ๊ณ„ ์ด๋™/๋’ค๋กœ๊ฐ€๊ธฐ๋Š” ํŽธ์ง‘ ๋ชจ๋“œ๋ฅผ ์œ ์ง€ํ•œ๋‹ค.
await store.send(.router(.routeAction(
id: 1, action: .onBoarding(.router(.routeAction(
id: 0, action: .InviteCode(.delegate(.presentSignUpName))
)))
))).finish()
await store.send(.router(.routeAction(
id: 1, action: .onBoarding(.router(.routeAction(
id: 1, action: .onBoardingName(.delegate(.presentBack))
)))
))).finish()
#expect(store.editGeneration)

if systemBack {
await store.send(.router(.updateRoutes(Array(store.routes.prefix(1))))).finish()
} else {
await store.send(.router(.routeAction(
id: 1, action: .onBoarding(.router(.routeAction(
id: 0, action: .InviteCode(.delegate(.presentBack))
)))
))).finish()
}
#expect(!store.editGeneration)
#expect(store.routes.count == 1)

await store.send(.router(.routeAction(id: 0, action: .profile(.async(.fetchUser))))).finish()
#expect(store.routes.first?.screen.profile?.viewState == .loaded)
#expect(store.routes.first?.screen.profile?.profile != nil)

// ๋‚ด ์ •๋ณด๋ฅผ ๋‹ซ์•˜๋‹ค ๋‹ค์‹œ ์—ด์–ด๋„ ๊ฐ™์€ ๊ณต์œ  ํ”Œ๋ž˜๊ทธ๊ฐ€ ์กฐํšŒ๋ฅผ ๋ง‰์ง€ ์•Š๋Š”๋‹ค.
let reopened = Store(initialState: ProfileCoordinator.State()) { ProfileCoordinator() }
await reopened.send(.router(.routeAction(id: 0, action: .profile(.async(.fetchUser))))).finish()
#expect(reopened.routes.first?.screen.profile?.profile != nil)
}
}

@Test("๊ธฐ์ˆ˜ ๋ณ€๊ฒฝ ํ›„ ํ™ˆ์ด๋‚˜ ๋กœ๊ทธ์ธ์œผ๋กœ ์ „ํ™˜ํ•˜๋ฉด ํŽธ์ง‘ ๋ชจ๋“œ๋ฅผ ์ข…๋ฃŒํ•œ๋‹ค", arguments: [
ProfileCoordinator.NavigationAction.presentMember,
.presentStaff,
.presentLogin
])
func generationEditNavigationClearsEditing(action: ProfileCoordinator.NavigationAction) async {
await withDependencies {
$0.defaultInMemoryStorage = InMemoryStorage()
} operation: {
let store = Store(initialState: ProfileCoordinator.State()) { ProfileCoordinator() }
await store.send(.router(.routeAction(
id: 0, action: .profile(.delegate(.presentEditGeneration))
))).finish()
#expect(store.editGeneration)
await store.send(.navigation(action)).finish()
#expect(!store.editGeneration)
}
}

@Test("Profile ๋กœ๊ทธ์•„์›ƒ์€ ํ™”๋ฉด์„ popํ•˜์ง€ ์•Š๊ณ  ๋กœ๊ทธ์ธ ์ „ํ™˜์„ ์š”์ฒญํ•œ๋‹ค")
func profileLogoutRequestsLoginWithoutPoppingRoutes() async {
let store = TestStore(initialState: ProfileCoordinator.State()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import ProfileDomainInterface
import Dependencies
import Sharing

public extension SharedReaderKey where Self == InMemoryKey<Bool>.Default {
/// ๊ธฐ์ˆ˜ ๋ณ€๊ฒฝ ํ™”๋ฉด์ด ์—ด๋ ค ์žˆ๋Š” ๋™์•ˆ๋งŒ ์œ ์ง€ํ•œ๋‹ค.
/// ์ด์ „ ๋ฒ„์ „์˜ UserDefaults ๊ฐ’์€ ์ฝ์ง€ ์•Š์•„ ์ค‘๋‹จ๋œ ํŽธ์ง‘์ด ๋‹ค์Œ ์‹คํ–‰์˜ ์กฐํšŒ๋ฅผ ๋ง‰์ง€ ์•Š๋Š”๋‹ค.
static var isEditingGeneration: Self {
Self[.inMemory("isEditingGeneration"), default: false]
}
}

public extension SharedReaderKey where Self == PersistentSharedKey<UserSession>.Default {
/// ์•ฑ ์‹คํ–‰ ์‚ฌ์ด์— ์œ ์ง€๋˜๋Š” ์‚ฌ์šฉ์ž ์„ธ์…˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct LoginFeature {

@Shared var userSession: UserSession
@Shared(.staffRole) var staffRole
@Shared(.appStorage("editGeneration")) var editGeneration: Bool = false
@Shared(.isEditingGeneration) var editGeneration
var currentSocialType: SocialType?
@Presents public var customAlert: CustomAlertState<CustomAlertAction>?

Expand Down Expand Up @@ -199,10 +199,9 @@ extension LoginFeature {
let role = login.role ?? .member
state.$staffRole.withLock { $0 = login.isNewUser ? nil : role }
state.$userSession.withLock { $0.userRole = role }
state.$editGeneration.withLock { $0 = false }

if login.isNewUser {
// ์‹ ๊ทœ ๊ฐ€์ž…: editGeneration ์ž”์žฌ(true)๋กœ ์ธํ•œ editProfile ์˜ค๋ถ„๊ธฐ ๋ฐฉ์ง€ โ†’ ํšŒ์›๊ฐ€์ž…(signUp) ๊ฐ•์ œ
state.$editGeneration.withLock { $0 = false }
return .send(.view(.showPolicyPopUp))
} else if role == .manager {
return .send(.delegate(.presentStaffMain))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,35 +135,38 @@ struct LoginActionCoverageTests {
/// ๊ธฐ์กด ๋ฉค๋ฒ„ ๋กœ๊ทธ์ธ์€ ์—ญํ• ์„ ์ €์žฅํ•˜๊ณ  ๋ฉค๋ฒ„ ๋ฉ”์ธ์œผ๋กœ ์ด๋™ํ•œ๋‹ค.
@Test("๊ธฐ์กด ๋ฉค๋ฒ„ ๋กœ๊ทธ์ธ ์„ฑ๊ณต์€ ์—ญํ•  ์ €์žฅ ํ›„ ๋ฉค๋ฒ„ ๋ฉ”์ธ์œผ๋กœ ์ด๋™ํ•œ๋‹ค")
func inner_loginResponse_existingMember_navigatesToMemberMain() async {
let store = Self.makeStore()
let store = Self.makeStore(configureState: { $0.editGeneration = true })

await store.send(.inner(.loginResponse(.success(LoginTestFixture.member)))) {
$0.staffRole = .member
$0.userSession.userRole = .member
$0.editGeneration = false
}
await store.receive(\.delegate.presentMemberMain)
}

/// ์šด์˜์ง„ ๋กœ๊ทธ์ธ์€ staffMain ์œผ๋กœ ๋ถ„๊ธฐํ•œ๋‹ค.
@Test("์šด์˜์ง„ ๋กœ๊ทธ์ธ ์„ฑ๊ณต์€ ์šด์˜์ง„ ๋ฉ”์ธ์œผ๋กœ ์ด๋™ํ•œ๋‹ค")
func inner_loginResponse_manager_navigatesToStaffMain() async {
let store = Self.makeStore()
let store = Self.makeStore(configureState: { $0.editGeneration = true })

await store.send(.inner(.loginResponse(.success(LoginTestFixture.manager)))) {
$0.staffRole = .manager
$0.userSession.userRole = .manager
$0.editGeneration = false
}
await store.receive(\.delegate.presentStaffMain)
}

/// role ์ด nil ์ธ ๊ฒฝ๊ณ„๊ฐ’์€ member ๋กœ ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ๋œ๋‹ค.
@Test("role ์ด nil ์ธ ๋กœ๊ทธ์ธ ์„ฑ๊ณต์€ member ๋กœ ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ๋œ๋‹ค")
func inner_loginResponse_nilRole_defaultsToMember() async {
let store = Self.makeStore()
let store = Self.makeStore(configureState: { $0.editGeneration = true })

await store.send(.inner(.loginResponse(.success(LoginTestFixture.roleless)))) {
$0.staffRole = .member
$0.userSession.userRole = .member
$0.editGeneration = false
}
await store.receive(\.delegate.presentMemberMain)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public struct SelectManagingFeature {
var managers: IdentifiedArrayOf<SelectManaging> = []

@Shared(.userSession) var userSession
@Shared(.appStorage("editGeneration")) var editGeneration: Bool = false
@Shared(.isEditingGeneration) var editGeneration
@Shared(.staffRole) var staffRole
@Presents var alert: AlertState<AlertAction>?
}
Expand Down Expand Up @@ -300,7 +300,6 @@ extension SelectManagingFeature {
}

case let .failure(error):
state.$editGeneration.withLock { $0 = false }
state.alert = AlertState {
TextState("ํ”„๋กœํ•„ ์ˆ˜์ • ์‹คํŒจ")
} actions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public struct SelectTeamFeature {


@Shared(.userSession) var userSession
@Shared(.appStorage("editGeneration")) var editGeneration: Bool = false
@Shared(.isEditingGeneration) var editGeneration
@Shared(.staffRole) var staffRole
@Presents var alert: AlertState<AlertAction>?
}
Expand Down Expand Up @@ -322,7 +322,6 @@ extension SelectTeamFeature {
}

case .failure(let error):
state.$editGeneration.withLock { $0 = false }
state.alert = AlertState {
TextState("ํ”„๋กœํ•„ ์ˆ˜์ • ์‹คํŒจ")
} actions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,18 @@ struct SelectManagingReducerTests {
func editGenerationNavigatesToMember() async {
let profile = OnBoardingCoverageFixture.memberProfile
let authUseCase = MockAuthRepository.refreshSuccess()
let appStorage = UserDefaults.inMemory
let inMemoryStorage = InMemoryStorage()
appStorage.set(true, forKey: "editGeneration")
let initialState = withDependencies {
$0.defaultInMemoryStorage = inMemoryStorage
} operation: {
var state = SelectManagingFeature.State()
state.$editGeneration.withLock { $0 = true }
return state
}

let store = TestStore(initialState: SelectManagingFeature.State()) {
let store = TestStore(initialState: initialState) {
SelectManagingFeature()
} withDependencies: {
$0.defaultAppStorage = appStorage
$0.defaultInMemoryStorage = inMemoryStorage
$0.profileUseCase = StubProfileUseCase(profile: profile)
$0.authUseCase = authUseCase
Expand Down Expand Up @@ -176,14 +180,18 @@ struct SelectManagingReducerTests {
@Test("๊ธฐ์ˆ˜ ๋ณ€๊ฒฝ ํ›„ ํ† ํฐ ๊ฐฑ์‹  ์‹คํŒจ ์‹œ ์šด์˜์ง„ API๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š๊ณ  ๋กœ๊ทธ์ธ์œผ๋กœ ์ด๋™ํ•œ๋‹ค")
func credentialRefreshFailureNavigatesToLogin() async {
let profile = OnBoardingCoverageFixture.managerProfile
let appStorage = UserDefaults.inMemory
let inMemoryStorage = InMemoryStorage()
appStorage.set(true, forKey: "editGeneration")
let initialState = withDependencies {
$0.defaultInMemoryStorage = inMemoryStorage
} operation: {
var state = SelectManagingFeature.State()
state.$editGeneration.withLock { $0 = true }
return state
}

let store = TestStore(initialState: SelectManagingFeature.State()) {
let store = TestStore(initialState: initialState) {
SelectManagingFeature()
} withDependencies: {
$0.defaultAppStorage = appStorage
$0.defaultInMemoryStorage = inMemoryStorage
$0.profileUseCase = StubProfileUseCase(profile: profile)
$0.authUseCase = MockAuthRepository.tokenExpired()
Expand Down Expand Up @@ -223,7 +231,7 @@ struct SelectManagingReducerTests {
await store.receive(\.delegate.presentManager)
}

@Test("ํ”„๋กœํ•„ ์ˆ˜์ • ์‹คํŒจ๋Š” ๊ธฐ์ˆ˜ ๋ณ€๊ฒฝ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋‚ด๋ฆฌ๊ณ  ์‹คํŒจ ์•Œ๋Ÿฟ์„ ํ‘œ์‹œํ•œ๋‹ค")
@Test("ํ”„๋กœํ•„ ์ˆ˜์ • ์‹คํŒจ๋Š” ๊ธฐ์ˆ˜ ๋ณ€๊ฒฝ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์‹คํŒจ ์•Œ๋Ÿฟ์„ ํ‘œ์‹œํ•œ๋‹ค")
func editProfileFailurePresentsAlert() async {
var state = SelectManagingFeature.State()
state.editGeneration = true
Expand All @@ -235,7 +243,6 @@ struct SelectManagingReducerTests {
let error = ProfileError.profileNotFound

await store.send(.inner(.editProfileResponse(.failure(error)))) {
$0.editGeneration = false
$0.alert = AlertState {
TextState("ํ”„๋กœํ•„ ์ˆ˜์ • ์‹คํŒจ")
} actions: {
Expand All @@ -248,6 +255,44 @@ struct SelectManagingReducerTests {
}
}

@Test("๊ธฐ์ˆ˜ ๋ณ€๊ฒฝ ์ˆ˜์ • ์‹คํŒจ ํ›„ ์žฌ์‹œ๋„๋Š” ํšŒ์›๊ฐ€์ž… ๋Œ€์‹  ํ”„๋กœํ•„ ์ˆ˜์ •์„ ๋‹ค์‹œ ํ˜ธ์ถœํ•œ๋‹ค")
func retryAfterEditProfileFailureKeepsEditFlow() async {
var state = SelectManagingFeature.State()
state.$editGeneration.withLock { $0 = true }

let store = TestStore(initialState: state) {
SelectManagingFeature()
} withDependencies: {
$0.profileUseCase = StubProfileUseCase(editFailure: .profileUpdateFailed)
$0.signUpUseCase = StubSignUpUseCase(failure: .accountAlreadyExists)
}
let error = ProfileError.from(EditProfileError.profileUpdateFailed)

for _ in 0..<2 {
await store.send(.view(.signUp))
await store.receive {
guard case .async(.editProfile) = $0 else { return false }
return true
}
await store.receive(\.inner) {
$0.alert = AlertState {
TextState("ํ”„๋กœํ•„ ์ˆ˜์ • ์‹คํŒจ")
} actions: {
ButtonState(action: .confirmTapped) {
TextState("ํ™•์ธ")
}
} message: {
TextState(error.errorDescription ?? "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.")
}
}
#expect(store.state.editGeneration)
await store.send(.scope(.alert(.dismiss))) {
$0.alert = nil
}
}
await store.finish()
}

// MARK: - Delegate / Scope / Binding

@Test("delegate ์•ก์…˜์€ ๋ชจ๋‘ ๋ถ€์ˆ˜ํšจ๊ณผ ์—†์ด ์†Œ๋น„๋œ๋‹ค")
Expand Down
Loading
Loading