diff --git a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift index 045d3ed6..29fe0dce 100644 --- a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift +++ b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift @@ -73,11 +73,12 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { try registerUseCaseDependency() try registerViewModelFactoryDependency() } catch let error as MHCoreError { - MHLogger.error("\(error.description)") + MHLogger.error(error.description + #function) } catch { - MHLogger.error("\(error.localizedDescription)") + MHLogger.error(error.localizedDescription + #function) } } + private func registerStorageDepedency() throws { DIContainer.shared.register(CoreDataStorage.self, object: CoreDataStorage()) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/View/CreateAudioViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/View/CreateAudioViewController.swift index 793c592b..6ff5c4c5 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/View/CreateAudioViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/View/CreateAudioViewController.swift @@ -122,7 +122,7 @@ final class CreateAudioViewController: UIViewController { configureAddSubviews() configureConstraints() configureAddActions() - input.send(.viewDidLoad) + input.send(.prepareTemporaryAudio) } // MARK: - Setup @@ -274,19 +274,11 @@ final class CreateAudioViewController: UIViewController { // MARK: - Helper private func requestMicrophonePermission() { - let alert = UIAlertController( - title: "마이크 권한 필요", - message: "설정에서 마이크 권한을 허용해주세요.", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in - self?.dismiss(animated: true) - }) Task { AVAudioSession.sharedInstance().requestRecordPermission { @Sendable granted in Task { @MainActor in if !granted { - self.present(alert, animated: true, completion: nil) + self.showRedirectSettingAlert(with: .audio) } } } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModel.swift index 86c8ae21..e29bbb0a 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModel.swift @@ -6,7 +6,7 @@ import MHDomain public final class CreateAudioViewModel: ViewModelType { // MARK: - Type enum Input { - case viewDidLoad + case prepareTemporaryAudio case audioButtonTapped case saveButtonTapped case recordCancelled @@ -39,8 +39,8 @@ public final class CreateAudioViewModel: ViewModelType { func transform(input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] event in switch event { - case .viewDidLoad: - Task { await self?.viewDidLoad() } + case .prepareTemporaryAudio: + Task { await self?.prepareTemporaryAudio() } case .audioButtonTapped: self?.audioButtonTapped() case .saveButtonTapped: @@ -54,14 +54,14 @@ public final class CreateAudioViewModel: ViewModelType { } // MARK: - Helper - private func viewDidLoad() async { + private func prepareTemporaryAudio() async { let mediaDescription = MediaDescription(type: .audio) self.mediaDescription = mediaDescription do { let url = try await temporaryStoreMediaUsecase.execute(media: mediaDescription) output.send(.audioFileURL(url: url)) } catch { - MHLogger.error("Error in store audio file url: \(error.localizedDescription)") + MHLogger.error(error.localizedDescription + #function) completion(nil) output.send(.recordCompleted) } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModelFactory.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModelFactory.swift index 0e6c6f16..d8eefe8c 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModelFactory.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Audio/Audio/ViewModel/CreateAudioViewModelFactory.swift @@ -15,4 +15,3 @@ public struct CreateAudioViewModelFactory { ) } } - diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Book/View/BookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Book/View/BookViewController.swift index 055acad2..2148f586 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Book/View/BookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Book/View/BookViewController.swift @@ -136,7 +136,7 @@ final class BookViewController: UIViewController { let editBookViewController = EditBookViewController(viewModel: editBookViewModel, mode: .modify) navigationController?.pushViewController(editBookViewController, animated: true) } catch { - MHLogger.error(error) + MHLogger.error(error.localizedDescription + #function) } } } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/View/BookCoverViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/View/BookCoverViewController.swift index a088a927..71e4305d 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/View/BookCoverViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/View/BookCoverViewController.swift @@ -129,7 +129,7 @@ final class BookCoverViewController: UIViewController { configureAddSubviews() configureConstraints() configureAction() - createInput.send(.viewDidAppear) + createInput.send(.setBookCover) modifyInput.send(.loadBookCover) } @@ -233,7 +233,7 @@ final class BookCoverViewController: UIViewController { let editBookViewController = EditBookViewController(viewModel: editBookViewModel) navigationController?.pushViewController(editBookViewController, animated: true) } catch { - MHLogger.error(error) + MHLogger.error(error.localizedDescription + #function) } } } @@ -489,7 +489,7 @@ extension BookCoverViewController: BookCategoryViewControllerDelegate { self.present(navigationController, animated: true) } catch { - MHLogger.error(error) + MHLogger.error(error.localizedDescription + #function) } } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/ViewModel/CreateBookCoverViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/ViewModel/CreateBookCoverViewModel.swift index b5b5a518..0ebc3d88 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/ViewModel/CreateBookCoverViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCover/ViewModel/CreateBookCoverViewModel.swift @@ -6,7 +6,7 @@ import Photos // TODO: - 에러 처리 필요 final class CreateBookCoverViewModel: ViewModelType { enum Input { - case viewDidAppear + case setBookCover case changedBookTitle(title: String?) case changedBookColor(colorIndex: Int) case changedBookImage(bookImage: Data?) @@ -60,7 +60,7 @@ final class CreateBookCoverViewModel: ViewModelType { func transform(input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] event in switch event { - case .viewDidAppear: + case .setBookCover: self?.setBookColor(nowIndex: 0) Task { try await self?.fetchMemorialHouseName() } case .changedBookTitle(let title): diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryTableViewCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Category/View/BookCategoryTableViewCell.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryTableViewCell.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Category/View/BookCategoryTableViewCell.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Category/View/BookCategoryViewController.swift similarity index 87% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryViewController.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Category/View/BookCategoryViewController.swift index 2b0325c8..800bbac3 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Category/View/BookCategoryViewController.swift @@ -41,7 +41,7 @@ final class BookCategoryViewController: UIViewController { setup() bind() - input.send(.viewDidLoad) + input.send(.fetchCategories) configureNavigationBar() configureConstraints() } @@ -62,11 +62,13 @@ final class BookCategoryViewController: UIViewController { private func bind() { let output = viewModel.transform(input: input.eraseToAnyPublisher()) - output.sink { [weak self] event in + output + .receive(on: DispatchQueue.main) + .sink { [weak self] event in switch event { case .createdCategory, .updatedCategory, .fetchCategories, .deletedCategory: self?.categoryTableView.reloadData() - case .failed(let errorMessage): + case .failure(let errorMessage): self?.showErrorAlert(with: errorMessage) } }.store(in: &cancellables) @@ -112,8 +114,6 @@ final class BookCategoryViewController: UIViewController { normal: normalAttributes, selected: selectedAttributes ) { [weak self] in - guard let self else { return } - let alert = UIAlertController( title: "카테고리 추가", message: "새로운 카테고리를 입력해주세요.", @@ -121,15 +121,11 @@ final class BookCategoryViewController: UIViewController { textField.placeholder = "카테고리 이름" }, confirmHandler: { [weak self] newText in - guard let newText, !newText.isEmpty else { - MHLogger.error("입력한 카테고리 이름이 유효하지 않습니다.") - return - } - self?.input.send(.addCategory(text: newText)) + self?.input.send(.createCategory(text: newText ?? "")) } ) - self.present(alert, animated: true) + self?.present(alert, animated: true) } } @@ -171,26 +167,20 @@ extension BookCategoryViewController: UITableViewDelegate { style: .normal, title: "수정" ) { [weak self] _, _, completion in - guard let self else { return } let alert = UIAlertController( title: "카테고리 수정", message: "수정할 카테고리 이름을 입력해주세요.", textFieldConfiguration: { textField in textField.placeholder = "카테고리 이름" - textField.text = self.viewModel.categories[indexPath.row].name + textField.text = self?.viewModel.categories[indexPath.row].name }, confirmHandler: { [weak self] newText in - guard let self, let newText = newText, !newText.isEmpty else { - MHLogger.error("수정할 카테고리 이름이 유효하지 않습니다.") - completion(false) - return - } - self.input.send(.updateCategory(index: indexPath.row, text: newText)) + self?.input.send(.updateCategory(index: indexPath.row, text: newText ?? "")) completion(true) } ) - self.present(alert, animated: true) + self?.present(alert, animated: true) } let deleteAction = UIContextualAction( diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Category/ViewModel/BookCategoryViewModel.swift similarity index 52% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryViewModel.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Category/ViewModel/BookCategoryViewModel.swift index bb04987d..7738c162 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Category/ViewModel/BookCategoryViewModel.swift @@ -4,8 +4,8 @@ import MHCore final class BookCategoryViewModel: ViewModelType { enum Input { - case viewDidLoad - case addCategory(text: String) + case createCategory(text: String) + case fetchCategories case updateCategory(index: Int, text: String) case deleteCategory(index: Int) } @@ -15,7 +15,7 @@ final class BookCategoryViewModel: ViewModelType { case fetchCategories case updatedCategory case deletedCategory - case failed(String) + case failure(String) } private let createBookCategoryUseCase: CreateBookCategoryUseCase @@ -52,12 +52,12 @@ final class BookCategoryViewModel: ViewModelType { input.sink { [weak self] event in Task { switch event { - case .viewDidLoad: + case .createCategory(let name): + await self?.createCategory(name: name) + case .fetchCategories: await self?.fetchCategories() - case .addCategory(let text): - await self?.createCategory(text: text) case .updateCategory(let index, let text): - await self?.updateCategory(index: index, text: text) + await self?.updateCategory(index: index, name: text) case .deleteCategory(let index): await self?.deleteCategory(index: index) } @@ -67,57 +67,59 @@ final class BookCategoryViewModel: ViewModelType { return output.eraseToAnyPublisher() } - // FIXME: MainActor 제거 - @MainActor - private func createCategory(text: String) async { + private func createCategory(name: String) async { + guard validateCategoryName(with: name) else { + MHLogger.error("카테고리 생성 유효성 검증 실패: \(name)" + #function) + output.send(.failure("카테고리 생성 유효성 검증 실패")) + return + } + do { - let category = BookCategory(order: categories.count, name: text) + let category = BookCategory(order: categories.count, name: name) try await createBookCategoryUseCase.execute(with: category) categories.append(category) output.send(.createdCategory) } catch { - MHLogger.error("카테고리를 생성하는데 실패했습니다: \(error)") - output.send(.failed("카테고리를 생성하는데 실패했습니다")) + MHLogger.error("카테고리를 생성하는데 실패했습니다: \(error)" + #function) + output.send(.failure("카테고리를 생성하는데 실패했습니다")) } } - @MainActor private func fetchCategories() async { do { let fetchedCategories = try await fetchBookCategoriesUseCase.execute() categories.append(contentsOf: fetchedCategories) output.send(.fetchCategories) } catch { - MHLogger.error("카테고리를 불러오는데 실패했습니다: \(error)") - output.send(.failed("카테고리를 불러오는데 실패했습니다")) + MHLogger.error("카테고리를 불러오는데 실패했습니다: \(error)" + #function) + output.send(.failure("카테고리를 불러오는데 실패했습니다")) } } - @MainActor - private func updateCategory(index: Int, text: String) async { - guard index >= 0 && index < categories.count else { - MHLogger.error("유효하지 않은 인덱스: \(index)") - output.send(.failed("유효하지 않은 인덱스: \(index)")) + private func updateCategory(index: Int, name: String) async { + guard validateIndex(index), + validateCategoryName(with: name) else { + MHLogger.error("카테고리 업데이트 유효성 검증 실패: \(name)" + #function) + output.send(.failure("카테고리 업데이트 유효성 검증 실패")) return } do { let oldName = categories[index].name - let category = BookCategory(order: index, name: text) + let category = BookCategory(order: index, name: name) try await updateBookCategoryUseCase.execute(oldName: oldName, with: category) categories[index] = category output.send(.updatedCategory) } catch { - MHLogger.error("카테고리를 업데이트하는데 실패했습니다: \(error)") - output.send(.failed("카테고리를 업데이트하는데 실패했습니다")) + MHLogger.error("카테고리를 업데이트하는데 실패했습니다: \(error)" + #function) + output.send(.failure("카테고리를 업데이트하는데 실패했습니다")) } } - @MainActor private func deleteCategory(index: Int) async { - guard index >= 0 && index < categories.count else { - MHLogger.error("유효하지 않은 인덱스: \(index)") - output.send(.failed("유효하지 않은 인덱스: \(index)")) + guard validateIndex(index) else { + MHLogger.error("카테고리 삭제 유효성 검증 실패: \(index)" + #function) + output.send(.failure("카테고리를 삭제하는데 실패했습니다")) return } @@ -127,8 +129,55 @@ final class BookCategoryViewModel: ViewModelType { categories.remove(at: index) output.send(.deletedCategory) } catch { - MHLogger.error("카테고리를 삭제하는데 실패했습니다: \(error)") - output.send(.failed("카테고리를 삭제하는데 실패했습니다")) + MHLogger.error("카테고리를 삭제하는데 실패했습니다: \(error)" + #function) + output.send(.failure("카테고리를 삭제하는데 실패했습니다")) + } + } + + // MARK: - 카테고리 유효성 검사 + private func validateCategoryName(with name: String) -> Bool { + return validateNonEmptyName(name) + && validateUniqueName(name) + && validateNameLength(name) + } + + // 인덱스 유효성 검사 + private func validateIndex(_ index: Int) -> Bool { + guard index >= 0 && index < categories.count else { + MHLogger.error("유효하지 않은 인덱스: \(index)" + #function) + output.send(.failure("유효하지 않은 인덱스: \(index)")) + return false + } + return true + } + + // 공백 또는 빈 문자열 검사 + private func validateNonEmptyName(_ name: String) -> Bool { + guard !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + MHLogger.error("카테고리 이름이 비어있거나 공백만 포함되어 있음: \(name)" + #function) + output.send(.failure("카테고리 이름은 공백일 수 없습니다.")) + return false + } + return true + } + + // 중복 이름 검사 + private func validateUniqueName(_ name: String) -> Bool { + guard !categories.contains(where: { $0.name == name }) else { + MHLogger.error("중복된 카테고리 이름: \(name)" + #function) + output.send(.failure("이미 존재하는 카테고리: \(name)")) + return false + } + return true + } + + // 이름 길이 검사 + private func validateNameLength(_ name: String) -> Bool { + guard name.count <= 10 else { + MHLogger.error("카테고리 이름이 너무 깁니다: \(name)" + #function) + output.send(.failure("카테고리 이름은 10자 이하여야 합니다.")) + return false } + return true } } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryViewModelFactory.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Category/ViewModel/BookCategoryViewModelFactory.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Category/BookCategoryViewModelFactory.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Category/ViewModel/BookCategoryViewModelFactory.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/View/CustomAlbumViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/View/CustomAlbumViewController.swift index 87385c30..389c5012 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/View/CustomAlbumViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/View/CustomAlbumViewController.swift @@ -176,20 +176,20 @@ final class CustomAlbumViewController: UIViewController { Task { @MainActor in guard let self = self else { return } if status == .authorized || status == .limited { - self.input.send(.viewDidLoad(mediaType: self.mediaType)) + self.input.send(.fetchPhotoAssets(mediaType: self.mediaType)) } else { self.dismiss(animated: true) } } } case .authorized, .limited: - input.send(.viewDidLoad(mediaType: mediaType)) + input.send(.fetchPhotoAssets(mediaType: mediaType)) case .restricted, .denied: showRedirectSettingAlert(with: .image) - MHLogger.info("앨범 접근 권한 거부로 뷰를 닫았습니다.") + MHLogger.info("앨범 접근 권한 거부로 뷰를 닫았습니다." + #function) default: showRedirectSettingAlert(with: .image) - MHLogger.error("알 수 없는 권한 상태로 인해 뷰를 닫았습니다.") + MHLogger.error("알 수 없는 권한 상태로 인해 뷰를 닫았습니다." + #function) } } @@ -210,10 +210,10 @@ final class CustomAlbumViewController: UIViewController { albumCollectionView.reloadData() case .restricted, .denied: showRedirectSettingAlert(with: .camera) - MHLogger.info("카메라 권한 거부") + MHLogger.info("카메라 권한 거부" + #function) default: showRedirectSettingAlert(with: .camera) - MHLogger.error(authorization) + MHLogger.error("\(authorization) 권한 상태" + #function) } } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/ViewModel/CustomAlbumViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/ViewModel/CustomAlbumViewModel.swift index d39e7f17..54558a46 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/ViewModel/CustomAlbumViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/CustomAlbum/ViewModel/CustomAlbumViewModel.swift @@ -4,7 +4,7 @@ import Combine final class CustomAlbumViewModel: ViewModelType { enum Input { - case viewDidLoad(mediaType: PHAssetMediaType) + case fetchPhotoAssets(mediaType: PHAssetMediaType) case photoDidChanged(to: PHChange) } @@ -20,7 +20,7 @@ final class CustomAlbumViewModel: ViewModelType { func transform(input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] event in switch event { - case .viewDidLoad(let mediaType): + case .fetchPhotoAssets(let mediaType): self?.fetchPhotoAssets(of: mediaType) case .photoDidChanged(let changeInstance): self?.updatePhotoAssets(changeInstance) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index e73f9a37..2ede6a59 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -109,10 +109,10 @@ final class EditBookViewController: UIViewController { configureNavigationBar() configureAddSubView() configureConstraints() - configureKeyboard() + configureKeyboardNotification() configureBinding() configureButtonAction() - input.send(.viewDidLoad) + input.send(.fetchBook) } // MARK: - Setup & Configuration @@ -206,7 +206,7 @@ final class EditBookViewController: UIViewController { trailing: editPageTableView.trailingAnchor, constantTrailing: 15 ) } - private func configureKeyboard() { + private func configureKeyboardNotification() { NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillAppear), @@ -215,7 +215,7 @@ final class EditBookViewController: UIViewController { ) NotificationCenter.default.addObserver( self, - selector: #selector(keyboardWillHide), + selector: #selector(keyboardWillDisappear), name: UIResponder.keyboardWillHideNotification, object: nil ) @@ -310,7 +310,8 @@ final class EditBookViewController: UIViewController { } // MARK: - Keyboard Appear & Hide - @objc private func keyboardWillAppear(_ notification: Notification) { + @objc + private func keyboardWillAppear(_ notification: Notification) { guard let keyboardInfo = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey], let keyboardSize = keyboardInfo as? CGRect else { return } let bottomConstant = -(keyboardSize.height - view.safeAreaInsets.bottom + 10) @@ -319,7 +320,8 @@ final class EditBookViewController: UIViewController { self?.view.layoutIfNeeded() } } - @objc private func keyboardWillHide() { + @objc + private func keyboardWillDisappear() { buttonStackViewBottomConstraint?.constant = Self.buttonBottomConstant UIView.animate(withDuration: 0.3) { [weak self] in self?.view.layoutIfNeeded() diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 1588f70f..029388a8 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -99,7 +99,7 @@ final class EditPageCell: UITableViewCell { case let .mediaLoadedWithURL(media, url): self?.mediaLoadedWithURL(media: media, url: url) case let .error(message): - MHLogger.error(message) // TODO: 더 좋은 처리가 필요함 + MHLogger.error(message + #function) // TODO: 더 좋은 처리가 필요함 } }.store(in: &cancellables) } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift index 9e483515..65947a57 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift @@ -6,7 +6,7 @@ import MHCore final class EditBookViewModel: ViewModelType { // MARK: - Type enum Input { - case viewDidLoad + case fetchBook case didAddMediaInTemporary(media: MediaDescription) case didAddMediaWithData(type: MediaType, attributes: [String: any Sendable]?, data: Data) case didAddMediaInURL(type: MediaType, attributes: [String: any Sendable]?, url: URL) @@ -61,7 +61,7 @@ final class EditBookViewModel: ViewModelType { func transform(input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] event in switch event { - case .viewDidLoad: + case .fetchBook: Task { await self?.fetchBook() } case let .didAddMediaInTemporary(media): Task { await self?.addMedia(media) } @@ -117,13 +117,16 @@ final class EditBookViewModel: ViewModelType { let description = MediaDescription(type: type, attributes: attributes) do { try await createMediaUseCase.execute(media: description, from: url, at: bookID) - try await deleteTemporaryMediaUsecase.execute(media: description) + if type == .audio { + try await deleteTemporaryMediaUsecase.execute(media: description) + } editPageViewModels[currentPageIndex].addMedia(media: description, url: url) } catch { output.send(.error(message: "미디어를 추가하는데 실패했습니다.")) MHLogger.error(error.localizedDescription + #function) } } + private func addMedia(_ description: MediaDescription) async { do { try await storeMediaUseCase.excute(media: description, to: bookID) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift index a188f197..45756727 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditPhoto/EditPhotoViewController.swift @@ -122,7 +122,7 @@ final class EditPhotoViewController: UIViewController { ) NotificationCenter.default.addObserver( self, - selector: #selector(keyboardWillHide), + selector: #selector(keyboardWillDisappear), name: UIResponder.keyboardWillHideNotification, object: nil ) @@ -284,7 +284,8 @@ final class EditPhotoViewController: UIViewController { } // MARK: - Keyboard Appear & Hide - @objc private func keyboardWillAppear(_ notification: Notification) { + @objc + private func keyboardWillAppear(_ notification: Notification) { guard let keyboardInfo = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey], let keyboardSize = keyboardInfo as? CGRect else { return } let bottomConstant = editButtonStackView.frame.height + view.safeAreaInsets.bottom @@ -294,7 +295,8 @@ final class EditPhotoViewController: UIViewController { } } - @objc private func keyboardWillHide() { + @objc + private func keyboardWillDisappear() { captionTextFieldBottomConstraint?.constant = -11 UIView.animate(withDuration: 0.3) { [weak self] in self?.view.layoutIfNeeded() diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/Notification+Keyboard.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/Notification+Keyboard.swift new file mode 100644 index 00000000..e5609563 --- /dev/null +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/Notification+Keyboard.swift @@ -0,0 +1,13 @@ +import UIKit + +extension UIViewController { + @objc + func keyboardWillShow(_ sender: Notification) { + self.view.frame.origin.y = -120 + } + + @objc + func keyboardWillHide(_ sender: Notification) { + self.view.frame.origin.y = 0 + } +} diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage+Resize.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage/UIImage+Resize.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage+Resize.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage/UIImage+Resize.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage+Rotate.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage/UIImage+Rotate.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage+Rotate.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIImage/UIImage+Rotate.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+Anchor.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+Anchor.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+Anchor.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+Anchor.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+Background.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+Background.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+Background.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+Background.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+CustomView.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+CustomView.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+CustomView.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+CustomView.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+SnapshotImage.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+SnapshotImage.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+SnapshotImage.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView/UIView+SnapshotImage.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+AudioSession.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+AudioSession.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+AudioSession.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+AudioSession.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+AuthorizationAlert.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+AuthorizationAlert.swift similarity index 88% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+AuthorizationAlert.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+AuthorizationAlert.swift index 7e366c66..d58d21a2 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+AuthorizationAlert.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+AuthorizationAlert.swift @@ -30,8 +30,11 @@ extension UIViewController { } }, cancelHandler: { [weak self] in - if content == .image { + switch content { + case .image, .camera: self?.navigationController?.popViewController(animated: true) + case .audio: + self?.dismiss(animated: true) } } ) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+ErrorAlert.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+ErrorAlert.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+ErrorAlert.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+ErrorAlert.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+HideKeyboard.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+HideKeyboard.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController+HideKeyboard.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIViewController/UIViewController+HideKeyboard.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Home/BookCollectionViewCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Home/View/BookCollectionViewCell.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Home/BookCollectionViewCell.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Home/View/BookCollectionViewCell.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Home/View/HomeViewController.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Home/View/HomeViewController.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Home/ViewModel/HomeViewModel.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewModel.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Home/ViewModel/HomeViewModel.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewModelFactory.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Home/ViewModel/HomeViewModelFactory.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewModelFactory.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Home/ViewModel/HomeViewModelFactory.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/View/ReadPageViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/View/ReadPageViewController.swift index 395771bf..032578aa 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/View/ReadPageViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/View/ReadPageViewController.swift @@ -51,7 +51,7 @@ final class ReadPageViewController: UIViewController { bind() setup() configureConstraints() - input.send(.viewDidLoad) + input.send(.loadPage) } private func bind() { diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/ViewModel/ReadPageViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/ViewModel/ReadPageViewModel.swift index 277a337c..04cd57e6 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/ViewModel/ReadPageViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/ReadPage/ViewModel/ReadPageViewModel.swift @@ -5,7 +5,7 @@ import Combine public final class ReadPageViewModel: ViewModelType { enum Input { - case viewDidLoad + case loadPage case didRequestMediaDataForData(media: MediaDescription) case didRequestMediaDataForURL(media: MediaDescription) } @@ -37,7 +37,7 @@ public final class ReadPageViewModel: ViewModelType { func transform(input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] event in switch event { - case .viewDidLoad: + case .loadPage: self?.output.send(.loadPage(page: self?.page)) case .didRequestMediaDataForData(let media): Task { await self?.loadMediaForData(media: media) } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Register/RegisterViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Register/View/RegisterViewController.swift similarity index 87% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Register/RegisterViewController.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Register/View/RegisterViewController.swift index dfb2dd41..41b7d230 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Register/RegisterViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Register/View/RegisterViewController.swift @@ -75,6 +75,7 @@ public final class RegisterViewController: UIViewController { setup() bind() + configureKeyboardNotification() configureAddSubview() configureConstraints() } @@ -121,6 +122,21 @@ public final class RegisterViewController: UIViewController { } } + private func configureKeyboardNotification() { + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillShow(_:)), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillHide(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) + } + private func configureAddSubview() { view.addSubview(coverImageView) view.addSubview(registerTextLabel) @@ -179,8 +195,21 @@ public final class RegisterViewController: UIViewController { } extension RegisterViewController: UITextFieldDelegate { - public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + public func textFieldShouldReturn( + _ textField: UITextField + ) -> Bool { textField.resignFirstResponder() return true } + + public func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + guard let currentText = textField.text as NSString? else { return false } + let newLength = currentText.length + string.count - range.length + + return newLength <= 10 + } } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Register/RegisterViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Register/ViewModel/RegisterViewModel.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Register/RegisterViewModel.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Register/ViewModel/RegisterViewModel.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Register/RegisterViewModelFactory.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Register/ViewModel/RegisterViewModelFactory.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Register/RegisterViewModelFactory.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Register/ViewModel/RegisterViewModelFactory.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Setting/SettingViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Setting/View/SettingViewController.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Setting/SettingViewController.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Setting/View/SettingViewController.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Setting/SettingViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Setting/ViewModel/SettingViewModel.swift similarity index 100% rename from MemorialHouse/MHPresentation/MHPresentation/Source/Setting/SettingViewModel.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/Setting/ViewModel/SettingViewModel.swift