Skip to content
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

기록소 등록 화면에서 홈 화면으로 전환 간의 엔티티 설계 변경 & 테스트 코드 작성 #110

Merged
merged 27 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ccf8744
chore: todo 주석 추가
Kyxxn Dec 1, 2024
997eb78
chore: 파일 위치 변경
Kyxxn Dec 1, 2024
71c7e35
refactor: Register -> Home 이동 실패에 대한 에러처리
Kyxxn Dec 1, 2024
681e092
refactor: MemorialHouse 모델 삭제에 따른 파일 수정
Kyxxn Dec 1, 2024
69b83dd
refactor: MemorialHouseName UseCase 구현
Kyxxn Dec 1, 2024
9e23665
chore: MemorialHouse ->MemorialHouseName으로 변경
Kyxxn Dec 1, 2024
5adeea7
feat: RegisterViewModel에 createMemorialHouseNameUseCase 추가
Kyxxn Dec 1, 2024
68c69f6
feat: RegisterViewModel 로직 구현
Kyxxn Dec 1, 2024
bb67f3a
feat: LocalMemorialHouseNameRepository 로직 구현
Kyxxn Dec 1, 2024
8070e62
feat: MemorialHouseNameStorage 로직 구현
Kyxxn Dec 1, 2024
37df6b9
feat: DIContainer에 기록소 이름과 관련된 의존성 등록
Kyxxn Dec 1, 2024
74e107d
feat: 홈 뷰모델에서 fetchMemorialHouseUseCase 로직 구현
Kyxxn Dec 1, 2024
e1730ce
chore: 홈뷰모델 UseCase 네이밍 변경
Kyxxn Dec 1, 2024
5d9f83e
refactor: 테스트를 위해 UserDefaults를 주입 받도록 변경
Kyxxn Dec 1, 2024
087a233
chore: 사용하지 않는 코드 제거
Kyxxn Dec 1, 2024
771bbda
fix: RegisterViewModel 테스트 가능한 구조로 변경
Kyxxn Dec 1, 2024
16a1471
refactor: 파라미터 네이밍 변경
Kyxxn Dec 1, 2024
d3d77e6
feat: RegisterViewModel 테스트 구현
Kyxxn Dec 1, 2024
9d9a807
refactor: Home에서 이름과 책커버들 받아오는 로직 분리
Kyxxn Dec 1, 2024
ad4e810
feat: HomeViewModel 테스트 구현
Kyxxn Dec 1, 2024
d4574b3
feat: MemorialHouseUseCaseTest 구현
Kyxxn Dec 1, 2024
c612cfb
feat: UserDefaultsMemorialHouseNameStorage Test 구현
Kyxxn Dec 1, 2024
2f291f1
refactor: SceneDelegate 로직 개선
Kyxxn Dec 1, 2024
1b2bf7a
feat: UserDefaults 쓰기 실패 에러코드 추가
Kyxxn Dec 1, 2024
a755670
refactor: UserDefaults 처리하는 Storage에서 Result로 반환
Kyxxn Dec 1, 2024
77127df
refactor: Register 뷰모델 로직 개선
Kyxxn Dec 1, 2024
b3b6b15
chore: 불필요한 코드 삭제
Kyxxn Dec 1, 2024
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 @@ -17,23 +17,58 @@
window = UIWindow(windowScene: windowScene)
registerDependency()

var initialViewController: UIViewController = RegisterViewController(viewModel: RegisterViewModel())
if UserDefaults.standard.object(forKey: Constant.houseNameUserDefaultKey) != nil {
do {
let viewModelFactory = try DIContainer.shared.resolve(HomeViewModelFactory.self)
let viewModel = viewModelFactory.make()
initialViewController = HomeViewController(viewModel: viewModel)
} catch {
MHLogger.error(error.localizedDescription)
}
}

let navigationController = UINavigationController(rootViewController: initialViewController)
window?.rootViewController = navigationController
let initialViewController = createInitialViewController()
window?.rootViewController = UINavigationController(rootViewController: initialViewController)
window?.makeKeyAndVisible()
}

func registerDependency() {
private func createInitialViewController() -> UIViewController {
if isUserRegistered() {
return createHomeViewController()
} else {
return createRegisterViewController()
}
}

private func isUserRegistered() -> Bool {
return UserDefaults.standard.object(forKey: Constant.houseNameUserDefaultKey) != nil
}

private func createHomeViewController() -> UIViewController {
do {
let homeViewModelFactory = try DIContainer.shared.resolve(HomeViewModelFactory.self)
let homeViewModel = homeViewModelFactory.make()
return HomeViewController(viewModel: homeViewModel)
} catch {
MHLogger.error("HomeViewModelFactory 해제 실패: \(error.localizedDescription)")
return createErrorViewController()
}
}

private func createRegisterViewController() -> UIViewController {
do {
let registerViewModelFactory = try DIContainer.shared.resolve(RegisterViewModelFactory.self)
let registerViewModel = registerViewModelFactory.make()
return RegisterViewController(viewModel: registerViewModel)
} catch {
MHLogger.error("CreateMemorialHouseNameUseCase 해제 실패: \(error.localizedDescription)")
return createErrorViewController()
}
}

private func createErrorViewController() -> UIViewController {
let errorViewController = UIViewController()
errorViewController.view.backgroundColor = .systemRed
let label = UILabel()
label.text = "오류가 발생했습니다."
label.textColor = .white
label.textAlignment = .center
label.frame = errorViewController.view.bounds
errorViewController.view.addSubview(label)
return errorViewController
}

private func registerDependency() {
do {
try registerStorageDepedency()
try registerRepositoryDependency()
Expand All @@ -48,7 +83,7 @@

private func registerStorageDepedency() throws {
DIContainer.shared.register(CoreDataStorage.self, object: CoreDataStorage())

let coreDataStorage = try DIContainer.shared.resolve(CoreDataStorage.self)
DIContainer.shared.register(
BookCategoryStorage.self,
Expand All @@ -62,12 +97,17 @@
BookStorage.self,
object: CoreDataBookStorage(coreDataStorage: coreDataStorage)
)
DIContainer.shared.register(
MemorialHouseNameStorage.self,
object: UserDefaultsMemorialHouseNameStorage()
)
}

private func registerRepositoryDependency() throws {
let memorialHouseNameStorage = try DIContainer.shared.resolve(MemorialHouseNameStorage.self)
DIContainer.shared.register(
MemorialHouseRepository.self,
object: DefaultMemorialHouseRepository()
MemorialHouseNameRepository.self,
object: LocalMemorialHouseNameRepository(storage: memorialHouseNameStorage)
)

let bookCategoryStorage = try DIContainer.shared.resolve(BookCategoryStorage.self)
Expand All @@ -87,12 +127,16 @@
)
}

private func registerUseCaseDependency() throws {

Check warning on line 130 in MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Function Body Length Violation: Function body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines (function_body_length)

Check warning on line 130 in MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Function Body Length Violation: Function body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines (function_body_length)
// MARK: MemorialHouse UseCase
let memorialHouseRepository = try DIContainer.shared.resolve(MemorialHouseRepository.self)
let memorialHouseNameRepository = try DIContainer.shared.resolve(MemorialHouseNameRepository.self)
DIContainer.shared.register(
FetchMemorialHouseUseCase.self,
object: DefaultFetchMemorialHouseUseCase(repository: memorialHouseRepository)
CreateMemorialHouseNameUseCase.self,
object: DefaultCreateMemorialHouseNameUseCase(repository: memorialHouseNameRepository)
)
DIContainer.shared.register(
FetchMemorialHouseNameUseCase.self,
object: DefaultFetchMemorialHouseNameUseCase(repository: memorialHouseNameRepository)
)

// MARK: Category UseCase
Expand Down Expand Up @@ -135,20 +179,33 @@

// MARK: - BookCover UseCase
let bookCoverRepository = try DIContainer.shared.resolve(BookCoverRepository.self)
DIContainer.shared.register(
FetchAllBookCoverUseCase.self,
object: DefaultFetchAllBookCoverUseCase(repository: bookCoverRepository)
)
DIContainer.shared.register(
UpdateBookCoverUseCase.self,
object: DefaultUpdateBookCoverUseCase(repository: bookCoverRepository)
)
}

private func registerViewModelFactoryDependency() throws {
// MARK: MemorialHouse ViewModel
let fetchMemorialHouseUseCase = try DIContainer.shared.resolve(FetchMemorialHouseUseCase.self)
// MARK: Register ViewModel
let createMemorialHouseNameUseCase = try DIContainer.shared.resolve(CreateMemorialHouseNameUseCase.self)
DIContainer.shared.register(
RegisterViewModelFactory.self,
object: RegisterViewModelFactory(createMemorialHouseNameUseCase: createMemorialHouseNameUseCase)
)

// MARK: Home ViewModel
let fetchMemorialHouseNameUseCase = try DIContainer.shared.resolve(FetchMemorialHouseNameUseCase.self)
let fetchAllBookCoverUseCase = try DIContainer.shared.resolve(FetchAllBookCoverUseCase.self)
let updateBookCoverUseCase = try DIContainer.shared.resolve(UpdateBookCoverUseCase.self)
DIContainer.shared.register(
HomeViewModelFactory.self,
object: HomeViewModelFactory(
fetchMemorialHouseUseCase: fetchMemorialHouseUseCase,
fetchMemorialHouseNameUseCase: fetchMemorialHouseNameUseCase,
fetchAllBookCoverUseCase: fetchAllBookCoverUseCase,
updateBookCoverUseCase: updateBookCoverUseCase
)
)
Expand Down
3 changes: 3 additions & 0 deletions MemorialHouse/MHCore/MHCore/MHDataError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public enum MHDataError: Error, CustomStringConvertible, Equatable {
case fileMovingFailure
case fileNotExists
case generalFailure
case setUserDefaultFailure

public var description: String {
switch self {
Expand Down Expand Up @@ -49,6 +50,8 @@ public enum MHDataError: Error, CustomStringConvertible, Equatable {
"파일이 존재하지 않습니다"
case .generalFailure:
"알 수 없는 에러입니다."
case .setUserDefaultFailure:
"UserDefault 설정 실패"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import MHFoundation
import MHCore

// TODO: 기록소 이름 변경

Check warning on line 4 in MemorialHouse/MHData/MHData/LocalStorage/MemorialHouseNameStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (기록소 이름 변경) (todo)

Check warning on line 4 in MemorialHouse/MHData/MHData/LocalStorage/MemorialHouseNameStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (기록소 이름 변경) (todo)
public protocol MemorialHouseNameStorage: Sendable {
func create(with memorialHouseName: String) async -> Result<Void, MHDataError>
func fetch() async -> Result<String, MHDataError>
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3: 개인적인 생각인데 MemorialHouseName은 UserDefault에만 저장될게 분명해서 굳이 한 번 추상화를 해줄 필요가 있나..? 라는 생각입니다. 그냥 Storage를 만들지 않고 바로 repository에서 처리해도 되지 않을까..? 라는 생각?? 입니당

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import MHCore
import MHFoundation

public struct UserDefaultsMemorialHouseNameStorage: MemorialHouseNameStorage {
private nonisolated(unsafe) let userDefaults: UserDefaults

public init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}

public func create(with memorialHouseName: String) async -> Result<Void, MHDataError> {
userDefaults.set(memorialHouseName, forKey: Constant.houseNameUserDefaultKey)

if userDefaults.string(forKey: Constant.houseNameUserDefaultKey) == memorialHouseName {
return .success(())
} else {
return .failure(.setUserDefaultFailure)
}
}

public func fetch() async -> Result<String, MHDataError> {
guard let memorialHouseName = userDefaults.string(forKey: Constant.houseNameUserDefaultKey) else {
MHLogger.error("MemorialHouseName을 찾을 수 없습니다: \(Constant.houseNameUserDefaultKey)")
return .failure(.noSuchEntity(key: Constant.houseNameUserDefaultKey))
}
return .success(memorialHouseName)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import MHCore
import MHDomain

public struct LocalMemorialHouseNameRepository: MemorialHouseNameRepository {
private let storage: MemorialHouseNameStorage

public init(storage: MemorialHouseNameStorage) {
self.storage = storage
}

public func createMemorialHouseName(with name: String) async -> Result<Void, MHDataError> {
return await storage.create(with: name)
}

public func fetchMemorialHouseName() async -> Result<String, MHDataError> {
return await storage.fetch()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3: 해당 파일은 UserDefault에서 불러오기 및 삭제 등이 실행되는지에 대한 테스트코드인 것 같은데, repository와 storage를 하나로 합치고 둘 중 하나의 테스트만 존재하면 되지 않을까.. 라는 생각이 드네용 ..

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Testing
@testable import MHData
@testable import MHCore
@testable import MHFoundation

struct UserDefaultsMemorialHouseNameStorageTest {
@Test func test저장소에_기록소_이름을_저장한다() async throws {
// Arrange
let suiteName = UUID().uuidString
let userDefaults = UserDefaults(suiteName: suiteName)!
let storage = UserDefaultsMemorialHouseNameStorage(userDefaults: userDefaults)
Comment on lines +9 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3: UserDefault를 UUID를 넣어 생성해주는 로직이 들어가게된 이유가 무엇인지 궁금합니다 .ᐟ.ᐟ

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

테스트 메소드마다 독립된 UserDefaults를 제공해주고 싶었는데,
String처리보다 UUID로 하는게 나을 것 같아 위처럼 작성해두었습니다 !

let testName = "테스트 기록소"

// Act
let result = await storage.create(with: testName)

// Assert
switch result {
case .success:
// 비동기 작업 후 일정 시간 대기 (필요 시)
let savedName = userDefaults.string(forKey: Constant.houseNameUserDefaultKey)
#expect(savedName == testName)
case .failure:
throw MHDataError.noSuchEntity(key: suiteName)
}

userDefaults.removePersistentDomain(forName: suiteName)
Copy link
Collaborator

Choose a reason for hiding this comment

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

팁: class를 통해 init deinit에 테스트 전 / 테스트 후 코드를 자동화 가능합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오오 감사합니다 !!
리팩토링 해볼게용 (리팩토링 기간에)

}

@Test func test_fetch_저장소에서_기록소_이름을_불러온다() async throws {
// Arrange
let suiteName = UUID().uuidString
let userDefaults = UserDefaults(suiteName: suiteName)!
let storage = UserDefaultsMemorialHouseNameStorage(userDefaults: userDefaults)
let testName = "테스트 기록소"
userDefaults.set(testName, forKey: Constant.houseNameUserDefaultKey)

// Act
let result = await storage.fetch()

// Assert
switch result {
case .success(let fetchedName):
#expect(fetchedName == testName)
case .failure:
throw MHDataError.noSuchEntity(key: suiteName)
}

userDefaults.removePersistentDomain(forName: suiteName)
}

@Test func test_fetch_기록소_이름이_없을때_에러를_반환한다() async throws {
// Arrange
let suiteName = UUID().uuidString
let userDefaults = UserDefaults(suiteName: suiteName)!
userDefaults.removePersistentDomain(forName: suiteName)
let storage = UserDefaultsMemorialHouseNameStorage(userDefaults: userDefaults)

// Act
let result = await storage.fetch()

// Assert
switch result {
case .success:
throw MHDataError.noSuchEntity(key: suiteName)
case .failure(let error):
#expect(error == .noSuchEntity(key: Constant.houseNameUserDefaultKey))
}

userDefaults.removePersistentDomain(forName: suiteName)
}
}
12 changes: 0 additions & 12 deletions MemorialHouse/MHDomain/MHDomain/Entity/MemorialHouse.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import MHCore

public protocol MemorialHouseNameRepository: Sendable {
func createMemorialHouseName(with name: String) async -> Result<Void, MHDataError>
func fetchMemorialHouseName() async -> Result<String, MHDataError>
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public struct DefaultFetchBookCoverUseCase: FetchBookCoverUseCase {
}
}

public struct DefaultFetchAllBookCoversUseCase: FetchAllBookCoverUseCase {
public struct DefaultFetchAllBookCoverUseCase: FetchAllBookCoverUseCase {
private let repository: BookCoverRepository

public init(repository: BookCoverRepository) {
Expand Down
Loading
Loading