From 071f40299bc5a98f2ab8bf9252f8f50d11c84607 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 25 Nov 2024 16:34:29 +0900 Subject: [PATCH 01/60] =?UTF-8?q?feat:=20MediaAttachment=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditBook/View/MediaAttachment.swift | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift new file mode 100644 index 00000000..0e164cd2 --- /dev/null +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift @@ -0,0 +1,50 @@ +import UIKit +import AVKit + +protocol MediaAttachmentDataSource: AnyObject { + func mediaAttachmentDragingImage(_ mediaAttachment: MediaAttachment, about view: UIView?) -> UIImage? +} + +final class MediaAttachment: NSTextAttachment { + var view: (UIView & MediaAttachable)? + var sourcePath: URL? { + get { + view?.sourcePath + } + set { + view?.sourcePath = newValue + } + } + weak var dataSource: MediaAttachmentDataSource? + + override func viewProvider(for parentView: UIView?, location: any NSTextLocation, textContainer: NSTextContainer?) -> NSTextAttachmentViewProvider? { + let provider = MediaAttachmentViewProvider( + textAttachment: self, + parentView: parentView, + textLayoutManager: textContainer?.textLayoutManager, + location: location + ) + provider.tracksTextAttachmentViewBounds = true + provider.view = view + return provider + } + override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? { + return dataSource?.mediaAttachmentDragingImage(self, about: view) + } +} + +class MediaAttachmentViewProvider: NSTextAttachmentViewProvider { + override func attachmentBounds( + for attributes: [NSAttributedString.Key: Any], + location: NSTextLocation, + textContainer: NSTextContainer?, + proposedLineFragment: CGRect, + position: CGPoint + ) -> CGRect { + return CGRect(x: 0, y: 0, width: proposedLineFragment.width, height: 300) + } +} + +protocol MediaAttachable { + var sourcePath: URL? { get set } +} From 3a59a319bb8ef9da74688ec2c252c2559f9c229e Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 25 Nov 2024 16:35:02 +0900 Subject: [PATCH 02/60] =?UTF-8?q?feat:=20View=EC=9D=98=20Snapshot=ED=8E=B8?= =?UTF-8?q?=EC=9D=98=EC=9A=A9=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/Extensions/UIView+SnapshotImage.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+SnapshotImage.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+SnapshotImage.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+SnapshotImage.swift new file mode 100644 index 00000000..ed6a23ce --- /dev/null +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Extensions/UIView+SnapshotImage.swift @@ -0,0 +1,10 @@ +import UIKit + +extension UIView { + func snapshotImage() -> UIImage? { + let renderer = UIGraphicsImageRenderer(bounds: bounds) + return renderer.image { context in + layer.render(in: context.cgContext) + } + } +} From b6e6d8381a53574ee7c957ae0e54323eb50fc263 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 25 Nov 2024 16:36:22 +0900 Subject: [PATCH 03/60] =?UTF-8?q?feat:=20EditPageCell=EC=97=90=20TextView?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B5=AC=ED=98=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 122 +++++++++++++++++- 1 file changed, 118 insertions(+), 4 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 4ef84eac..2dc02193 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -1,4 +1,5 @@ import UIKit +import MHCore final class EditPageCell: UITableViewCell { // MARK: - Property @@ -18,9 +19,7 @@ final class EditPageCell: UITableViewCell { return textView }() - private var textLayoutManager: NSTextLayoutManager? private var textStorage: NSTextStorage? - private var textContainer: NSTextContainer? // MARK: - Initializer override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -44,9 +43,7 @@ final class EditPageCell: UITableViewCell { backgroundColor = .clear selectionStyle = .none - textLayoutManager = textView.textLayoutManager textStorage = textView.textStorage - textContainer = textView.textContainer } private func configureAddSubView() { addSubview(textView) @@ -66,4 +63,121 @@ final class EditPageCell: UITableViewCell { super.touchesBegan(touches, with: event) } + + // MARK: - Method + /// Storage에서 Text와 Attachment 정보를 추출해냅니다. + private func separateStorageInformation(_ textStorage: NSTextStorage) -> (NSAttributedString, [String: Any]) { + var attachmentMetadata: [String: Any] = [:] + let mutableAttributedString = NSMutableAttributedString(attributedString: textStorage) + + textStorage.enumerateAttribute(.attachment, in: NSRange(location: 0, length: textStorage.length)) { value, range, _ in + if let mediaAttachment = value as? MediaAttachment, + let url = mediaAttachment.sourcePath { + // 위치와 URL 저장 + attachmentMetadata[String(range.location)] = url.absoluteString + // Placeholder로 텍스트 대체 + mutableAttributedString.replaceCharacters(in: range, with: " ") + } + } + + return (mutableAttributedString, attachmentMetadata) + } + /// Text와 Attachment 정보를 하나의 문자열로 조합합니다. + private func mergeStorageInformation(text savedAttributedString: NSAttributedString, attachmentMetaData: [String: Any]) -> NSAttributedString { + let mutableAttributedString = NSMutableAttributedString(attributedString: savedAttributedString) + + attachmentMetaData + .map { (Int($0.key) ?? 0, $0.value as? String ?? "") } + .forEach { location, urlString in + let range = NSRange(location: location, length: 1) + let mediaAttachment = MediaAttachment() + mediaAttachment.sourcePath = URL(string: urlString) + let attachmentString = NSAttributedString(attachment: mediaAttachment) + + // Placeholder(공백) 교체 + mutableAttributedString.replaceCharacters(in: range, with: attachmentString) + } + + return mutableAttributedString + } + private func saveTextWithAttachments() { + guard let textStorage = textStorage else { return } + + let documentDirectory = FileManager.default.urls( + for: .documentDirectory, + in: .userDomainMask + ).first! + let (savedText, metadata) = separateStorageInformation(textStorage) + + do { + let fileURL = documentDirectory.appendingPathComponent("textWithAttachments.rtf") + let rtfData = try savedText.data( + from: NSRange(location: 0, length: savedText.length), + documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf] + ) + try rtfData.write(to: fileURL) + MHLogger.debug("Text with attachments saved to \(fileURL)") + } catch { + MHLogger.error("Error saving text with attachments: \(error)") + } + + do { + let metadataFileURL = documentDirectory.appendingPathComponent("attachmentMetadata.json") + let metadataData = try JSONSerialization.data(withJSONObject: metadata, options: []) + try metadataData.write(to: metadataFileURL) + MHLogger.debug("Attachment metadata saved to \(metadataFileURL)") + } catch { + MHLogger.error("Error saving attachment metadata: \(error)") + } + } + private func loadTextWithAttachments() { + let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let fileURL = documentDirectory.appendingPathComponent("textWithAttachments.rtf") + do { + let rtfData = try Data(contentsOf: fileURL) + let savedText = try NSAttributedString( + data: rtfData, + options: [.documentType: NSAttributedString.DocumentType.rtf], + documentAttributes: nil + ) + let metadataFileURL = documentDirectory.appendingPathComponent("attachmentMetadata.json") + let metadataData = try Data(contentsOf: metadataFileURL) + let metadata = try JSONSerialization.jsonObject(with: metadataData) as! [String: Any] + // 커스텀 NSTextAttachment로 변환 + let restoredText = mergeStorageInformation(text: savedText, attachmentMetaData: metadata) + textStorage?.setAttributedString(restoredText) + MHLogger.debug("Text with attachments loaded from \(fileURL)") + } catch { + MHLogger.error("Error loading text with attachments: \(error)") + } + } + + private func isAcceptableHight(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText attributedText: NSAttributedString) -> Bool { + let updatedText = NSMutableAttributedString(attributedString: textView.attributedText) + let textViewWidth = textView.bounds.width + let temporaryTextView = UITextView( + frame: CGRect(x: 0, y: 0, width: textViewWidth, height: .greatestFiniteMagnitude) + ) + updatedText.append(attributedText) + updatedText.replaceCharacters(in: range, with: attributedText) + temporaryTextView.attributedText = updatedText + temporaryTextView.sizeToFit() + + return temporaryTextView.contentSize.height <= textView.bounds.height + || attributedText.string.isEmpty + } +} + +// MARK: - MediaAttachmentDataSource +extension EditPageCell: @preconcurrency MediaAttachmentDataSource { + func mediaAttachmentDragingImage(_ mediaAttachment: MediaAttachment, about view: UIView?) -> UIImage? { + view?.snapshotImage() + } +} + +// MARK: - UITextViewDelegate +extension EditPageCell: UITextViewDelegate { + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + return isAcceptableHight(textView, shouldChangeTextIn: range, replacementText: .init(string: text)) + } } From 977d3002d2a7e0fc880879fb41efebab032dfdf0 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 25 Nov 2024 16:51:39 +0900 Subject: [PATCH 04/60] =?UTF-8?q?feat:=20Page=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHDomain/Entity/MediaDescription.swift | 14 ++++++++++++++ .../MHDomain/MHDomain/Entity/MediaType.swift | 5 +++++ .../MHDomain/MHDomain/Entity/Page.swift | 17 +++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 MemorialHouse/MHDomain/MHDomain/Entity/MediaDescription.swift create mode 100644 MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift create mode 100644 MemorialHouse/MHDomain/MHDomain/Entity/Page.swift diff --git a/MemorialHouse/MHDomain/MHDomain/Entity/MediaDescription.swift b/MemorialHouse/MHDomain/MHDomain/Entity/MediaDescription.swift new file mode 100644 index 00000000..d943dcd1 --- /dev/null +++ b/MemorialHouse/MHDomain/MHDomain/Entity/MediaDescription.swift @@ -0,0 +1,14 @@ +import Foundation + +public struct MediaDescription: Identifiable { + public let id: UUID + public let type: MediaType + + public init( + id: UUID, + type: MediaType + ) { + self.id = id + self.type = type + } +} diff --git a/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift b/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift new file mode 100644 index 00000000..041527b1 --- /dev/null +++ b/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift @@ -0,0 +1,5 @@ +public enum MediaType { + case image + case video + case audio +} diff --git a/MemorialHouse/MHDomain/MHDomain/Entity/Page.swift b/MemorialHouse/MHDomain/MHDomain/Entity/Page.swift new file mode 100644 index 00000000..8c8effba --- /dev/null +++ b/MemorialHouse/MHDomain/MHDomain/Entity/Page.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct Page: Identifiable { + public let id: UUID + public let metadata: [Int: MediaDescription] + public let text: String + + public init( + id: UUID, + metadata: [Int : MediaDescription], + text: String + ) { + self.id = id + self.metadata = metadata + self.text = text + } +} From b86d39d917e13d0f6d3ed8cdd799905ecc100505 Mon Sep 17 00:00:00 2001 From: iceHood Date: Thu, 28 Nov 2024 16:52:41 +0900 Subject: [PATCH 05/60] =?UTF-8?q?feat:=20media=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHDomain/MHDomain/Entity/MediaType.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift b/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift index e2dad1f2..6aff8c17 100644 --- a/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift +++ b/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift @@ -2,4 +2,15 @@ public enum MediaType: String, Sendable { case image case video case audio + + var defaultFileExtension: String { + switch self { + case .image: + return "png" + case .video: + return "mp4" + case .audio: + return "m4a" + } + } } From 24fc2ddb53646ac82f5164bf2c566e69190da8e7 Mon Sep 17 00:00:00 2001 From: iceHood Date: Thu, 28 Nov 2024 16:53:01 +0900 Subject: [PATCH 06/60] =?UTF-8?q?feat:=20book=20usecase=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHDomain/UseCase/DefaultBookUseCase.swift | 61 +++++++++++++++++++ .../UseCase/Interface/BookUseCase.swift | 14 +++++ 2 files changed, 75 insertions(+) create mode 100644 MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift create mode 100644 MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift new file mode 100644 index 00000000..eecda096 --- /dev/null +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift @@ -0,0 +1,61 @@ +import MHFoundation + +public struct DefaultCreateBookUseCase: CreateBookUseCase { + // MARK: - Property + let repository: BookRepository + + // MARK: - Initializer + public init(repository: BookRepository) { + self.repository = repository + } + + // MARK: - Method + public func execute(book: Book) async throws { + try await repository.create(book: book).get() + } +} + +public struct DefaultFetchBookUseCase: FetchBookUseCase { + // MARK: - Property + let repository: BookRepository + + // MARK: - Initializer + public init(repository: BookRepository) { + self.repository = repository + } + + // MARK: - Method + public func execute(bookID: UUID) async throws -> Book { + try await repository.fetch(bookID: bookID).get() + } +} + +public struct DefaultUpdateBookUseCase: UpdateBookUseCase { + // MARK: - Property + let repository: BookRepository + + // MARK: - Initializer + public init(repository: BookRepository) { + self.repository = repository + } + + // MARK: - Method + public func execute(book: Book) async throws { + try await repository.update(bookID: book.id, to: book).get() + } +} + +public struct DefaultDeleteBookUseCase: DeleteBookUseCase { + // MARK: - Property + let repository: BookRepository + + // MARK: - Initializer + public init(repository: BookRepository) { + self.repository = repository + } + + // MARK: - Method + public func execute(bookID: UUID) async throws { + try await repository.delete(bookID: bookID).get() + } +} diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift new file mode 100644 index 00000000..3e87acd8 --- /dev/null +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift @@ -0,0 +1,14 @@ +import MHFoundation + +public protocol CreateBookUseCase { + func execute(book: Book) async throws +} +public protocol FetchBookUseCase { + func execute(bookID: UUID) async throws -> Book +} +public protocol UpdateBookUseCase { + func execute(book: Book) async throws +} +public protocol DeleteBookUseCase { + func execute(bookID: UUID) async throws +} From 9d8fb5fa5503a1f75f89702eec7c2a9c55ee6c40 Mon Sep 17 00:00:00 2001 From: iceHood Date: Thu, 28 Nov 2024 16:53:11 +0900 Subject: [PATCH 07/60] =?UTF-8?q?feat:=20media=20usecase=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UseCase/DefaultMediaUseCase.swift | 80 +++++++++++++++++++ .../UseCase/Interface/MediaUseCase.swift | 19 +++++ 2 files changed, 99 insertions(+) create mode 100644 MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift create mode 100644 MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift new file mode 100644 index 00000000..f1bc7d79 --- /dev/null +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift @@ -0,0 +1,80 @@ +import MHFoundation +import MHCore + +public struct DefaultCreateMediaUseCase: CreateMediaUseCase { + // MARK: - Property + let repository: MediaRepository + + // MARK: - Initializer + public init(repository: MediaRepository) { + self.repository = repository + } + + // MARK: - Method + public func execute(media: MediaDescription, data: Data) async throws { + try await repository.create(media: media, data: data, to: nil).get() + } + public func execute(media: MediaDescription, in url: URL) async throws { + try await repository.create(media: media, from: url, to: nil).get() + } +} + +public struct DefaultFetchMediaUseCase: FetchMediaUseCase { + // MARK: - Property + let repository: MediaRepository + + // MARK: - Initializer + public init(repository: MediaRepository) { + self.repository = repository + } + + // MARK: - Method + public func execute(media: MediaDescription, in bookID: UUID) async throws -> Data { + do { + return try await repository.read(media: media, from: nil).get() + } catch { + return try await repository.read(media: media, from: bookID).get() + } + } + public func execute(media: MediaDescription, in bookID: UUID) async throws -> URL { + do { + return try await repository.getURL(media: media, from: nil).get() + } catch { + return try await repository.getURL(media: media, from: bookID).get() + } + } +} + +public struct DefaultDeleteMediaUseCase: DeleteMediaUseCase { + // MARK: - Property + let repository: MediaRepository + + // MARK: - Initializer + public init(repository: MediaRepository) { + self.repository = repository + } + + // MARK: - Method + public func execute(media: MediaDescription, in bookID: UUID) async throws { + do { + return try await repository.delete(media: media, at: nil).get() + } catch { + return try await repository.delete(media: media, at: bookID).get() + } + } +} + +public struct DefaultPersistentlyStoreMediaUseCase: PersistentlyStoreMediaUseCase { + // MARK: - Property + let repository: MediaRepository + + // MARK: - Initializer + public init(repository: MediaRepository) { + self.repository = repository + } + + // MARK: - Method + public func execute(to bookID: UUID) async throws { + try await repository.moveAllTemporaryMedia(to: bookID).get() + } +} diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift new file mode 100644 index 00000000..c2154167 --- /dev/null +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift @@ -0,0 +1,19 @@ +import MHFoundation + +public protocol CreateMediaUseCase { + func execute(media: MediaDescription, data: Data) async throws + func execute(media: MediaDescription, in url: URL) async throws +} + +public protocol FetchMediaUseCase { + func execute(media: MediaDescription, in bookID: UUID) async throws -> Data + func execute(media: MediaDescription, in bookID: UUID) async throws -> URL +} + +public protocol DeleteMediaUseCase { + func execute(media: MediaDescription, in bookID: UUID) async throws +} + +public protocol PersistentlyStoreMediaUseCase { + func execute(to bookID: UUID) async throws +} From c6571b952194f76a26912735bcf127b0c2fc1d6f Mon Sep 17 00:00:00 2001 From: iceHood Date: Thu, 28 Nov 2024 16:53:45 +0900 Subject: [PATCH 08/60] =?UTF-8?q?feat:=20bookViewModel=20Usecase=EC=99=80?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/EditBookViewModel.swift | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift index 81764860..64f7480d 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift @@ -1,9 +1,14 @@ +import MHFoundation import Combine +import MHDomain final class EditBookViewModel: ViewModelType { // MARK: - Type enum Input { case viewDidLoad + case didAddMediaWithData(type: MediaType, at: Int, data: Data) + case didAddMediaInURL(type: MediaType, at: Int, url: URL) + case didSaveButtonTapped } enum Output { case setTableView @@ -12,20 +17,60 @@ final class EditBookViewModel: ViewModelType { // MARK: - Property private let output = PassthroughSubject() private var cancellables = Set() - private var pages = [String]() + private let fetchBookUseCase: FetchBookUseCase + private let updateBookUseCase: UpdateBookUseCase + private let storeBookUseCase: PersistentlyStoreMediaUseCase + private let bookID: UUID + private var book: Book? + + // MARK: - Initializer + init( + fetchBookUseCase: FetchBookUseCase, + updateBookUseCase: UpdateBookUseCase, + storeBookUseCase: PersistentlyStoreMediaUseCase, + bookID: UUID + ) { + self.fetchBookUseCase = fetchBookUseCase + self.updateBookUseCase = updateBookUseCase + self.storeBookUseCase = storeBookUseCase + self.bookID = bookID + } // MARK: - Method func transform(input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] event in switch event { case .viewDidLoad: - self?.fetchPages() + self?.fetchBook() + case let .didAddMediaWithData(type, at, data): + self?.addMedia(type: type, at: at, with: data) + case let .didAddMediaInURL(type, at, url): + self?.addMedia(type: type, at: at, in: url) + case .didSaveButtonTapped: + <#code#> } }.store(in: &cancellables) return output.eraseToAnyPublisher() } - private func fetchPages() { - // TODO: - Page가져오는 로직 추가 + private func fetchBook() { + Task { + book = try await fetchBookUseCase.execute(bookID: bookID) + } + } + private func addMedia(type: MediaType, at index: Int, with data: Data) { + let description = MediaDescription( + id: UUID(), + type: type + ) + } + private func addMedia(type: MediaType, at index: Int, in url: URL) { + let description = MediaDescription( + id: UUID(), + type: type + ) + + } + } From 662755610eaa61e28d4ebbcaa10003fb70a683ef Mon Sep 17 00:00:00 2001 From: iceHood Date: Thu, 28 Nov 2024 16:54:09 +0900 Subject: [PATCH 09/60] =?UTF-8?q?feat:=20Media=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0=20=EC=A7=84=ED=96=89...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/Design/MHPolaroidPhotoView.swift | 14 +++++ .../Source/EditBook/View/EditPageCell.swift | 43 ++++++++------- .../EditBook/View/MediaAttachment.swift | 53 +++++++++++++++---- 3 files changed, 81 insertions(+), 29 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHPolaroidPhotoView.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHPolaroidPhotoView.swift index 0d1ff2ce..a2e37f03 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHPolaroidPhotoView.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHPolaroidPhotoView.swift @@ -1,4 +1,5 @@ import UIKit +import MHDomain final class MHPolaroidPhotoView: UIView { // MARK: - UI Components @@ -78,3 +79,16 @@ final class MHPolaroidPhotoView: UIView { creationDateLabel.text = creationDate } } + +extension MHPolaroidPhotoView: @preconcurrency MediaAttachable { + // TODO: - 임시로 넣어 놓음 일단 + var mediaType: MediaType { + get { + .image + } + } + func configureSource(with path: URL?) { + guard let path else { return } + photoImageView.image = UIImage(contentsOfFile: path.absoluteString) + } +} diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 2dc02193..2fc9557e 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -1,5 +1,7 @@ import UIKit +import MHDomain import MHCore +import MHData final class EditPageCell: UITableViewCell { // MARK: - Property @@ -66,32 +68,39 @@ final class EditPageCell: UITableViewCell { // MARK: - Method /// Storage에서 Text와 Attachment 정보를 추출해냅니다. - private func separateStorageInformation(_ textStorage: NSTextStorage) -> (NSAttributedString, [String: Any]) { - var attachmentMetadata: [String: Any] = [:] + private func separateStorageInformation( + _ textStorage: NSTextStorage + ) -> (String, [Int: MediaDescription]) { + var metaData = [Int: MediaDescription]() let mutableAttributedString = NSMutableAttributedString(attributedString: textStorage) textStorage.enumerateAttribute(.attachment, in: NSRange(location: 0, length: textStorage.length)) { value, range, _ in - if let mediaAttachment = value as? MediaAttachment, - let url = mediaAttachment.sourcePath { + if let mediaAttachment = value as? MediaAttachment { // 위치와 URL 저장 - attachmentMetadata[String(range.location)] = url.absoluteString + metaData[range.location] = mediaAttachment.mediaDescription // Placeholder로 텍스트 대체 mutableAttributedString.replaceCharacters(in: range, with: " ") } } - - return (mutableAttributedString, attachmentMetadata) + return (mutableAttributedString.string, metaData) } /// Text와 Attachment 정보를 하나의 문자열로 조합합니다. - private func mergeStorageInformation(text savedAttributedString: NSAttributedString, attachmentMetaData: [String: Any]) -> NSAttributedString { + private func mergeStorageInformation( + text savedAttributedString: NSAttributedString, + attachmentMetaData: [Int: MediaDescription] + ) -> NSAttributedString { let mutableAttributedString = NSMutableAttributedString(attributedString: savedAttributedString) attachmentMetaData - .map { (Int($0.key) ?? 0, $0.value as? String ?? "") } - .forEach { location, urlString in + .forEach { + location, + description in let range = NSRange(location: location, length: 1) - let mediaAttachment = MediaAttachment() - mediaAttachment.sourcePath = URL(string: urlString) + let mediaAttachment = MediaAttachment( + view: MHPolaroidPhotoView(), // TODO: - 이거 바꿔줘야함... + description: description + ) + mediaAttachment.mediaDescription = description let attachmentString = NSAttributedString(attachment: mediaAttachment) // Placeholder(공백) 교체 @@ -110,12 +119,7 @@ final class EditPageCell: UITableViewCell { let (savedText, metadata) = separateStorageInformation(textStorage) do { - let fileURL = documentDirectory.appendingPathComponent("textWithAttachments.rtf") - let rtfData = try savedText.data( - from: NSRange(location: 0, length: savedText.length), - documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf] - ) - try rtfData.write(to: fileURL) + MHFileManager().create(at: <#T##String#>, fileName: <#T##String#>, data: <#T##Data#>) MHLogger.debug("Text with attachments saved to \(fileURL)") } catch { MHLogger.error("Error saving text with attachments: \(error)") @@ -142,7 +146,7 @@ final class EditPageCell: UITableViewCell { ) let metadataFileURL = documentDirectory.appendingPathComponent("attachmentMetadata.json") let metadataData = try Data(contentsOf: metadataFileURL) - let metadata = try JSONSerialization.jsonObject(with: metadataData) as! [String: Any] + let metadata = try JSONSerialization.jsonObject(with: metadataData) as? [String: Any] // 커스텀 NSTextAttachment로 변환 let restoredText = mergeStorageInformation(text: savedText, attachmentMetaData: metadata) textStorage?.setAttributedString(restoredText) @@ -151,7 +155,6 @@ final class EditPageCell: UITableViewCell { MHLogger.error("Error loading text with attachments: \(error)") } } - private func isAcceptableHight(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText attributedText: NSAttributedString) -> Bool { let updatedText = NSMutableAttributedString(attributedString: textView.attributedText) let textViewWidth = textView.bounds.width diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift index 0e164cd2..ff660c1d 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift @@ -1,22 +1,33 @@ import UIKit import AVKit +import MHDomain protocol MediaAttachmentDataSource: AnyObject { func mediaAttachmentDragingImage(_ mediaAttachment: MediaAttachment, about view: UIView?) -> UIImage? } final class MediaAttachment: NSTextAttachment { - var view: (UIView & MediaAttachable)? - var sourcePath: URL? { - get { - view?.sourcePath - } - set { - view?.sourcePath = newValue + // MARK: - Property + private let view: (UIView & MediaAttachable) + var mediaDescription: MediaDescription { + didSet { + view.configureSource(with: mediaDescription) } } weak var dataSource: MediaAttachmentDataSource? + // MARK: - Initializer + init(view: (UIView & MediaAttachable), description: MediaDescription) { + self.view = view + self.mediaDescription = description + super.init(data: nil, ofType: nil) + } + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError() + } + + // MARK: - ViewConfigures override func viewProvider(for parentView: UIView?, location: any NSTextLocation, textContainer: NSTextContainer?) -> NSTextAttachmentViewProvider? { let provider = MediaAttachmentViewProvider( textAttachment: self, @@ -26,14 +37,37 @@ final class MediaAttachment: NSTextAttachment { ) provider.tracksTextAttachmentViewBounds = true provider.view = view + provider.type = mediaDescription.type return provider } override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? { return dataSource?.mediaAttachmentDragingImage(self, about: view) } + + // MARK: - Method + func setup() { + view.configureSource(with: mediaDescription) + } } class MediaAttachmentViewProvider: NSTextAttachmentViewProvider { + // MARK: - Property + var type: MediaType? + private var height: CGFloat { + switch type { // TODO: - 조정 필요 + case .image: + 300 + case .video: + 200 + case .audio: + 100 + case nil: + 10 + default: + 100 + } + } + override func attachmentBounds( for attributes: [NSAttributedString.Key: Any], location: NSTextLocation, @@ -41,10 +75,11 @@ class MediaAttachmentViewProvider: NSTextAttachmentViewProvider { proposedLineFragment: CGRect, position: CGPoint ) -> CGRect { - return CGRect(x: 0, y: 0, width: proposedLineFragment.width, height: 300) + return CGRect(x: 0, y: 0, width: proposedLineFragment.width, height: height) } } +// MARK: - MediaAttachable protocol MediaAttachable { - var sourcePath: URL? { get set } + func configureSource(with description: MediaDescription) } From 08e4eea332966ecf94b2f3d2326076cf35c04980 Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:35:05 +0900 Subject: [PATCH 10/60] =?UTF-8?q?feat:=20sceneDelegate=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/App/SceneDelegate.swift | 83 ++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift index 0253fca7..aa5b64a0 100644 --- a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift +++ b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift @@ -35,6 +35,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { func registerDependency() { do { + registerStorage() registerRepositoryDependency() try registerUseCaseDependency() try registerViewModelFactoryDependency() @@ -44,7 +45,25 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { MHLogger.error("\(error.localizedDescription)") } } - + private func registerStorage() { + let coreDataStorage = CoreDataStorage() + DIContainer.shared.register( + CoreDataStorage.self, + object: coreDataStorage + ) + DIContainer.shared.register( + CoreDataBookCoverStorage.self, + object: CoreDataBookCoverStorage(coreDataStorage: coreDataStorage) + ) + DIContainer.shared.register( + CoreDataBookStorage.self, + object: CoreDataBookStorage(coreDataStorage: coreDataStorage) + ) + DIContainer.shared.register( + MHFileManager.self, + object: MHFileManager(directoryType: .documentDirectory) + ) + } private func registerRepositoryDependency() { DIContainer.shared.register( MemorialHouseRepository.self, @@ -54,6 +73,21 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { CategoryRepository.self, object: DefaultCategoryRepository() ) + guard let bookCoverStorage = try? DIContainer.shared.resolve(CoreDataBookCoverStorage.self) else { return } + DIContainer.shared.register( + LocalBookCoverRepository.self, + object: LocalBookCoverRepository(storage: bookCoverStorage) + ) + guard let bookStorage = try? DIContainer.shared.resolve(CoreDataBookStorage.self) else { return } + DIContainer.shared.register( + BookRepository.self, + object: LocalBookRepository(storage: bookStorage) + ) + guard let fileManager = try? DIContainer.shared.resolve(MHFileManager.self) else { return } + DIContainer.shared.register( + MediaRepository.self, + object: LocalMediaRepository(storage: fileManager) + ) } private func registerUseCaseDependency() throws { @@ -82,6 +116,34 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { DeleteCategoryUseCase.self, object: DefaultDeleteCategoryUseCase(repository: categoryRepository) ) + + // MARK: - EditBook UseCase + let bookRepository = try DIContainer.shared.resolve(BookRepository.self) + DIContainer.shared.register( + FetchBookUseCase.self, + object: DefaultFetchBookUseCase(repository: bookRepository) + ) + DIContainer.shared.register( + UpdateBookUseCase.self, + object: DefaultUpdateBookUseCase(repository: bookRepository) + ) + let mediaRepository = try DIContainer.shared.resolve(MediaRepository.self) + DIContainer.shared.register( + PersistentlyStoreMediaUseCase.self, + object: DefaultPersistentlyStoreMediaUseCase(repository: mediaRepository) + ) + DIContainer.shared.register( + CreateMediaUseCase.self, + object: DefaultCreateMediaUseCase(repository: mediaRepository) + ) + DIContainer.shared.register( + FetchMediaUseCase.self, + object: DefaultFetchMediaUseCase(repository: mediaRepository) + ) + DIContainer.shared.register( + DeleteMediaUseCase.self, + object: DefaultDeleteMediaUseCase(repository: mediaRepository) + ) } private func registerViewModelFactoryDependency() throws { @@ -108,5 +170,24 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { deleteCategoryUseCase: deleteCategoryUseCase ) ) + + // MARK: - EditBook ViewModel + let fetchBookUseCase = try DIContainer.shared.resolve(FetchBookUseCase.self) + let updateBookUseCase = try DIContainer.shared.resolve(UpdateBookUseCase.self) + let storeBookUseCase = try DIContainer.shared.resolve(PersistentlyStoreMediaUseCase.self) + let createMediaUseCase = try DIContainer.shared.resolve(CreateMediaUseCase.self) + let fetchMediaUseCase = try DIContainer.shared.resolve(FetchMediaUseCase.self) + let deleteMediaUseCase = try DIContainer.shared.resolve(DeleteMediaUseCase.self) + DIContainer.shared.register( + EditBookViewModelFactory.self, + object: EditBookViewModelFactory( + fetchBookUseCase: fetchBookUseCase, + updateBookUseCase: updateBookUseCase, + storeBookUseCase: storeBookUseCase, + createMediaUseCase: createMediaUseCase, + fetchMediaUseCase: fetchMediaUseCase, + deleteMediaUseCase: deleteMediaUseCase + ) + ) } } From 56371076db620ab216bfbc5ef7821639535710d0 Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:36:26 +0900 Subject: [PATCH 11/60] =?UTF-8?q?refactor:=20Public=EB=B0=8F=20Sendable?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHData/LocalStorage/BookStorage.swift | 2 +- .../CoreData/CoreDataBookCoverStorage.swift | 12 +++++----- .../CoreData/CoreDataBookStorage.swift | 12 +++++----- .../CoreData/CoreDataStorage.swift | 15 ++++++------- .../FileManager/MHFileManager.swift | 22 +++++++++---------- .../MHData/LocalStorage/FileStorage.swift | 2 +- .../Repository/LocalMediaRepository.swift | 2 +- .../MHDomain/Repository/BookRepository.swift | 2 +- .../MHDomain/Repository/MediaRepository.swift | 2 +- .../UseCase/DefaultMediaUseCase.swift | 2 +- .../UseCase/Interface/BookUseCase.swift | 8 +++---- .../UseCase/Interface/MediaUseCase.swift | 8 +++---- 12 files changed, 44 insertions(+), 45 deletions(-) diff --git a/MemorialHouse/MHData/MHData/LocalStorage/BookStorage.swift b/MemorialHouse/MHData/MHData/LocalStorage/BookStorage.swift index bcc1e25a..edb3fcbc 100644 --- a/MemorialHouse/MHData/MHData/LocalStorage/BookStorage.swift +++ b/MemorialHouse/MHData/MHData/LocalStorage/BookStorage.swift @@ -1,7 +1,7 @@ import MHFoundation import MHCore -public protocol BookStorage { +public protocol BookStorage: Sendable { func create(data: BookDTO) async -> Result func fetch(with id: UUID) async -> Result func update(with id: UUID, data: BookDTO) async -> Result diff --git a/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookCoverStorage.swift b/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookCoverStorage.swift index 48ec82d0..f75ea29b 100644 --- a/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookCoverStorage.swift +++ b/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookCoverStorage.swift @@ -2,16 +2,16 @@ import MHFoundation import MHCore import CoreData -final class CoreDataBookCoverStorage { +public final class CoreDataBookCoverStorage { private let coreDataStorage: CoreDataStorage - init(coreDataStorage: CoreDataStorage) { + public init(coreDataStorage: CoreDataStorage) { self.coreDataStorage = coreDataStorage } } extension CoreDataBookCoverStorage: BookCoverStorage { - func create(data: BookCoverDTO) async -> Result { + public func create(data: BookCoverDTO) async -> Result { let context = coreDataStorage.persistentContainer.viewContext do { try await context.perform { @@ -38,7 +38,7 @@ extension CoreDataBookCoverStorage: BookCoverStorage { } } - func fetch() async -> Result<[BookCoverDTO], MHDataError> { + public func fetch() async -> Result<[BookCoverDTO], MHDataError> { let context = coreDataStorage.persistentContainer.viewContext do { var bookCoverEntities: [BookCoverEntity] = [] @@ -57,7 +57,7 @@ extension CoreDataBookCoverStorage: BookCoverStorage { } } - func update(with id: UUID, data: BookCoverDTO) async -> Result { + public func update(with id: UUID, data: BookCoverDTO) async -> Result { let context = coreDataStorage.persistentContainer.viewContext do { try await context.perform { [weak self] in @@ -83,7 +83,7 @@ extension CoreDataBookCoverStorage: BookCoverStorage { } } - func delete(with id: UUID) async -> Result { + public func delete(with id: UUID) async -> Result { let context = coreDataStorage.persistentContainer.viewContext do { try await context.perform { [weak self] in diff --git a/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookStorage.swift b/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookStorage.swift index 9f0fce1c..66629d16 100644 --- a/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookStorage.swift +++ b/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookStorage.swift @@ -2,16 +2,16 @@ import MHFoundation import MHCore import CoreData -final class CoreDataBookStorage { +public final class CoreDataBookStorage { private let coreDataStorage: CoreDataStorage - init(coreDataStorage: CoreDataStorage) { + public init(coreDataStorage: CoreDataStorage) { self.coreDataStorage = coreDataStorage } } extension CoreDataBookStorage: BookStorage { - func create(data: BookDTO) async -> Result { + public func create(data: BookDTO) async -> Result { let context = coreDataStorage.persistentContainer.viewContext do { try await context.perform { [weak self] in @@ -34,7 +34,7 @@ extension CoreDataBookStorage: BookStorage { return .failure(.createEntityFailure) } } - func fetch(with id: UUID) async -> Result { + public func fetch(with id: UUID) async -> Result { let context = coreDataStorage.persistentContainer.viewContext do { @@ -57,7 +57,7 @@ extension CoreDataBookStorage: BookStorage { return .failure(.fetchEntityFaliure) } } - func update(with id: UUID, data: BookDTO) async -> Result { + public func update(with id: UUID, data: BookDTO) async -> Result { do { let context = coreDataStorage.persistentContainer.viewContext try await context.perform { [weak self] in @@ -80,7 +80,7 @@ extension CoreDataBookStorage: BookStorage { return .failure(.updateEntityFailure) } } - func delete(with id: UUID) async -> Result { + public func delete(with id: UUID) async -> Result { do { let context = coreDataStorage.persistentContainer.viewContext try await context.perform { [weak self] in diff --git a/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataStorage.swift b/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataStorage.swift index 0f7617e6..19e87e59 100644 --- a/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataStorage.swift +++ b/MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataStorage.swift @@ -1,7 +1,7 @@ import CoreData import MHCore -class CoreDataStorage { +public final class CoreDataStorage: Sendable { static let modelName: String = "MemorialHouseModel" nonisolated(unsafe) static let memorialHouseModel: NSManagedObjectModel = { @@ -14,17 +14,16 @@ class CoreDataStorage { return NSManagedObjectModel(contentsOf: modelURL)! }() - lazy var persistentContainer: NSPersistentContainer = { - let container = NSPersistentContainer(name: CoreDataStorage.modelName) + let persistentContainer: NSPersistentContainer + + public init() { + let container = NSPersistentContainer(name: CoreDataStorage.modelName, managedObjectModel: Self.memorialHouseModel) container.loadPersistentStores { _, error in guard let error else { return } MHLogger.error("\(#function): PersistentContainer 호출에 실패; \(error.localizedDescription)") } - - return container - }() - - init() { } + self.persistentContainer = container + } func saveContext() async { let context = persistentContainer.viewContext diff --git a/MemorialHouse/MHData/MHData/LocalStorage/FileManager/MHFileManager.swift b/MemorialHouse/MHData/MHData/LocalStorage/FileManager/MHFileManager.swift index 4294e354..cf2fdc78 100644 --- a/MemorialHouse/MHData/MHData/LocalStorage/FileManager/MHFileManager.swift +++ b/MemorialHouse/MHData/MHData/LocalStorage/FileManager/MHFileManager.swift @@ -1,17 +1,17 @@ import MHFoundation import MHCore -struct MHFileManager { - private let fileManager = FileManager.default +public struct MHFileManager: Sendable { + private var fileManager: FileManager { FileManager.default } private let directoryType: FileManager.SearchPathDirectory - init(directoryType: FileManager.SearchPathDirectory) { + public init(directoryType: FileManager.SearchPathDirectory) { self.directoryType = directoryType } } extension MHFileManager: FileStorage { - func create(at path: String, fileName name: String, data: Data) async -> Result { + public func create(at path: String, fileName name: String, data: Data) async -> Result { guard let directory = fileManager.urls( for: directoryType, in: .userDomainMask @@ -28,7 +28,7 @@ extension MHFileManager: FileStorage { return .failure(.fileCreationFailure) } } - func read(at path: String, fileName name: String) async -> Result { + public func read(at path: String, fileName name: String) async -> Result { guard let directory = fileManager.urls( for: directoryType, in: .userDomainMask @@ -47,7 +47,7 @@ extension MHFileManager: FileStorage { return .failure(.fileReadingFailure) } } - func delete(at path: String, fileName name: String) async -> Result { + public func delete(at path: String, fileName name: String) async -> Result { guard let directory = fileManager.urls( for: directoryType, in: .userDomainMask @@ -63,7 +63,7 @@ extension MHFileManager: FileStorage { return .failure(.fileDeletionFailure) } } - func copy(at url: URL, to newPath: String, newFileName name: String) async -> Result { + public func copy(at url: URL, to newPath: String, newFileName name: String) async -> Result { let originDataPath = url guard fileManager.fileExists(atPath: originDataPath.path) else { @@ -86,7 +86,7 @@ extension MHFileManager: FileStorage { return .failure(.fileMovingFailure) } } - func copy(at path: String, fileName name: String, to newPath: String) async -> Result { + public func copy(at path: String, fileName name: String, to newPath: String) async -> Result { guard let originDirectory = fileManager.urls( for: directoryType, in: .userDomainMask @@ -115,7 +115,7 @@ extension MHFileManager: FileStorage { return .failure(.fileMovingFailure) } } - func move(at path: String, fileName name: String, to newPath: String) async -> Result { + public func move(at path: String, fileName name: String, to newPath: String) async -> Result { guard let originDirectory = fileManager.urls( for: directoryType, in: .userDomainMask @@ -144,7 +144,7 @@ extension MHFileManager: FileStorage { return .failure(.fileMovingFailure) } } - func moveAll(in path: String, to newPath: String) async -> Result { + public func moveAll(in path: String, to newPath: String) async -> Result { guard let originDirectory = fileManager.urls( for: directoryType, in: .userDomainMask @@ -174,7 +174,7 @@ extension MHFileManager: FileStorage { return .failure(.fileMovingFailure) } } - func getURL(at path: String, fileName name: String) async -> Result { + public func getURL(at path: String, fileName name: String) async -> Result { guard let originDirectory = fileManager.urls( for: directoryType, in: .userDomainMask diff --git a/MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift b/MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift index 89b2f1fe..4a3d6296 100644 --- a/MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift +++ b/MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift @@ -1,7 +1,7 @@ import MHFoundation import MHCore -public protocol FileStorage { +public protocol FileStorage: Sendable { /// 지정된 경로에 파일을 생성합니다. /// Documents폴더에 파일을 생성합니다. /// 중간 경로 폴더를 자동으로 생성합니다. diff --git a/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift b/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift index 7f641528..7da25579 100644 --- a/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift +++ b/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift @@ -4,7 +4,7 @@ import MHDomain import MHCore import AVFoundation -public struct LocalMediaRepository: MediaRepository { +public struct LocalMediaRepository: MediaRepository, Sendable { private let storage: FileStorage public init(storage: FileStorage) { diff --git a/MemorialHouse/MHDomain/MHDomain/Repository/BookRepository.swift b/MemorialHouse/MHDomain/MHDomain/Repository/BookRepository.swift index 15f9f067..44d51acb 100644 --- a/MemorialHouse/MHDomain/MHDomain/Repository/BookRepository.swift +++ b/MemorialHouse/MHDomain/MHDomain/Repository/BookRepository.swift @@ -1,7 +1,7 @@ import MHFoundation import MHCore -public protocol BookRepository { +public protocol BookRepository: Sendable { func create(book: Book) async -> Result func fetch(bookID id: UUID) async -> Result func update(bookID id: UUID, to book: Book) async -> Result diff --git a/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift b/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift index b6756919..65ff7b3c 100644 --- a/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift +++ b/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift @@ -2,7 +2,7 @@ import MHFoundation import MHCore import Photos -public protocol MediaRepository { +public protocol MediaRepository: Sendable { func create(media mediaDescription: MediaDescription, data: Data, to bookID: UUID?) async -> Result func create(media mediaDescription: MediaDescription, from: URL, to bookID: UUID?) async -> Result func read(media mediaDescription: MediaDescription, from bookID: UUID?) async -> Result diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift index f1bc7d79..a4038763 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift @@ -1,7 +1,7 @@ import MHFoundation import MHCore -public struct DefaultCreateMediaUseCase: CreateMediaUseCase { +public struct DefaultCreateMediaUseCase: CreateMediaUseCase, Sendable { // MARK: - Property let repository: MediaRepository diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift index 3e87acd8..ce515cec 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/BookUseCase.swift @@ -1,14 +1,14 @@ import MHFoundation -public protocol CreateBookUseCase { +public protocol CreateBookUseCase: Sendable { func execute(book: Book) async throws } -public protocol FetchBookUseCase { +public protocol FetchBookUseCase: Sendable { func execute(bookID: UUID) async throws -> Book } -public protocol UpdateBookUseCase { +public protocol UpdateBookUseCase: Sendable { func execute(book: Book) async throws } -public protocol DeleteBookUseCase { +public protocol DeleteBookUseCase: Sendable { func execute(bookID: UUID) async throws } diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift index c2154167..eaaa009a 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift @@ -1,19 +1,19 @@ import MHFoundation -public protocol CreateMediaUseCase { +public protocol CreateMediaUseCase: Sendable { func execute(media: MediaDescription, data: Data) async throws func execute(media: MediaDescription, in url: URL) async throws } -public protocol FetchMediaUseCase { +public protocol FetchMediaUseCase: Sendable { func execute(media: MediaDescription, in bookID: UUID) async throws -> Data func execute(media: MediaDescription, in bookID: UUID) async throws -> URL } -public protocol DeleteMediaUseCase { +public protocol DeleteMediaUseCase: Sendable { func execute(media: MediaDescription, in bookID: UUID) async throws } -public protocol PersistentlyStoreMediaUseCase { +public protocol PersistentlyStoreMediaUseCase: Sendable { func execute(to bookID: UUID) async throws } From 1478723c15a6fab9e46e6193488e75b2bef0f23c Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:37:12 +0900 Subject: [PATCH 12/60] =?UTF-8?q?feat:=20EditBookViewModel=20=EC=9D=B4=20P?= =?UTF-8?q?ageVIewModel=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/EditBookViewModel.swift | 73 +++++++++++++------ 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift index 64f7480d..8add122c 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift @@ -1,13 +1,14 @@ import MHFoundation -import Combine +@preconcurrency import Combine import MHDomain +import MHCore final class EditBookViewModel: ViewModelType { // MARK: - Type enum Input { - case viewDidLoad - case didAddMediaWithData(type: MediaType, at: Int, data: Data) - case didAddMediaInURL(type: MediaType, at: Int, url: URL) + case viewDidLoad(bookID: UUID) + case didAddMediaWithData(type: MediaType, atPage: Int, data: Data) + case didAddMediaInURL(type: MediaType, atPage: Int, url: URL) case didSaveButtonTapped } enum Output { @@ -20,57 +21,85 @@ final class EditBookViewModel: ViewModelType { private let fetchBookUseCase: FetchBookUseCase private let updateBookUseCase: UpdateBookUseCase private let storeBookUseCase: PersistentlyStoreMediaUseCase - private let bookID: UUID - private var book: Book? + private let createMediaUseCase: CreateMediaUseCase + private let fetchMediaUseCase: FetchMediaUseCase + private let deleteMediaUseCase: DeleteMediaUseCase + private var bookID: UUID? + private var pageViewModels: [EditPageViewModel] = [] // MARK: - Initializer init( fetchBookUseCase: FetchBookUseCase, updateBookUseCase: UpdateBookUseCase, storeBookUseCase: PersistentlyStoreMediaUseCase, - bookID: UUID + createMediaUseCase: CreateMediaUseCase, + fetchMediaUseCase: FetchMediaUseCase, + deleteMediaUseCase: DeleteMediaUseCase ) { self.fetchBookUseCase = fetchBookUseCase self.updateBookUseCase = updateBookUseCase self.storeBookUseCase = storeBookUseCase - self.bookID = bookID + self.createMediaUseCase = createMediaUseCase + self.fetchMediaUseCase = fetchMediaUseCase + self.deleteMediaUseCase = deleteMediaUseCase } - // MARK: - Method + // MARK: - Binding Method func transform(input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] event in switch event { - case .viewDidLoad: - self?.fetchBook() - case let .didAddMediaWithData(type, at, data): - self?.addMedia(type: type, at: at, with: data) - case let .didAddMediaInURL(type, at, url): - self?.addMedia(type: type, at: at, in: url) + case let .viewDidLoad(bookID): + Task { await self?.fetchBook(bookID: bookID) } + case let .didAddMediaWithData(type, atPage, data): + self?.addMedia(type: type, at: atPage, with: data) + case let .didAddMediaInURL(type, atPage, url): + self?.addMedia(type: type, at: atPage, in: url) case .didSaveButtonTapped: - <#code#> + Task { await self?.saveMediaAll() } } }.store(in: &cancellables) return output.eraseToAnyPublisher() } - private func fetchBook() { - Task { - book = try await fetchBookUseCase.execute(bookID: bookID) + private func fetchBook(bookID: UUID) async { + self.bookID = bookID + guard let book = try? await fetchBookUseCase.execute(bookID: bookID) else { return } + book.pages.forEach { page in + let pageViewModel = EditPageViewModel( + createMediaUseCase: createMediaUseCase, + fetchMediaUseCase: fetchMediaUseCase, + deleteMediaUseCase: deleteMediaUseCase, + bookID: bookID, + page: page + ) + pageViewModels.append(pageViewModel) } + output.send(.setTableView) } private func addMedia(type: MediaType, at index: Int, with data: Data) { let description = MediaDescription( id: UUID(), type: type ) - + pageViewModels[index].addMedia(media: description, data: data) } private func addMedia(type: MediaType, at index: Int, in url: URL) { let description = MediaDescription( id: UUID(), type: type ) - + pageViewModels[index].addMedia(media: description, url: url) + } + private func saveMediaAll() async { + guard let bookID else { return } + try? await storeBookUseCase.execute(to: bookID) + } + + // MARK: - Method + func numberOfPages() -> Int { + return pageViewModels.count + } + func page(at index: Int) -> EditPageViewModel { + return pageViewModels[index] } - } From d9407a227244eaebd92b7bea70029512d0cf2f91 Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:37:30 +0900 Subject: [PATCH 13/60] =?UTF-8?q?feat:=20PageViewModel=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/EditPageViewModel.swift | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift new file mode 100644 index 00000000..980da11a --- /dev/null +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift @@ -0,0 +1,114 @@ +import MHFoundation +@preconcurrency import Combine +import MHDomain +import MHCore + +final class EditPageViewModel: ViewModelType { + // MARK: - Type + enum Input { + case pageWillAppear + case pageWillDisappear(attributedText: NSAttributedString) + case didRequestMediaDataForData(media: MediaDescription) + case didRequestMediaDataForURL(media: MediaDescription) + } + enum Output { + case page(page: Page) + case mediaAddedWithData(media: MediaDescription, data: Data) + case mediaAddedWithURL(media: MediaDescription, url: URL) + case mediaLoadedWithData(media: MediaDescription, data: Data) + case mediaLoadedWithURL(media: MediaDescription, url: URL) + } + + // MARK: - Property + private let output = PassthroughSubject() + private var cancellables = Set() + private let createMediaUseCase: CreateMediaUseCase + private let fetchMediaUseCase: FetchMediaUseCase + private let deleteMediaUseCase: DeleteMediaUseCase + private var bookID: UUID + private var page: Page + + // MARK: - Initializer + init( + createMediaUseCase: CreateMediaUseCase, + fetchMediaUseCase: FetchMediaUseCase, + deleteMediaUseCase: DeleteMediaUseCase, + bookID: UUID, + page: Page + ) { + self.createMediaUseCase = createMediaUseCase + self.fetchMediaUseCase = fetchMediaUseCase + self.deleteMediaUseCase = deleteMediaUseCase + self.bookID = bookID + self.page = page + } + + // MARK: - Binding Method + func transform(input: AnyPublisher) -> AnyPublisher { + input.sink { [weak self] event in + switch event { + case .pageWillAppear: + self?.pageWillAppear() + case .pageWillDisappear(let attributedText): + self?.pageWillDisappear(text: attributedText) + case .didRequestMediaDataForData(let media): + Task { await self?.loadMediaForData(media: media) } + case .didRequestMediaDataForURL(let media): + Task { await self?.loadMediaForURL(media: media) } + } + }.store(in: &cancellables) + + return output.eraseToAnyPublisher() + } + private func pageWillAppear() { + output.send(.page(page: page)) + } + private func pageWillDisappear(text: NSAttributedString) { + let page = converTextToPage(text: text) + self.page = page + } + private func loadMediaForData(media: MediaDescription) async { + guard let mediaData: Data = try? await fetchMediaUseCase.execute(media: media, in: bookID) else { return } + output.send(.mediaLoadedWithData(media: media, data: mediaData)) + } + + // MARK: - Method + func addMedia(media: MediaDescription, data: Data) { + output.send(.mediaAddedWithData(media: media, data: data)) + } + func addMedia(media: MediaDescription, url: URL) { + output.send(.mediaAddedWithURL(media: media, url: url)) + } + + // MARK: - Helper + private func loadMediaForURL(media: MediaDescription) async { + guard let mediaURL: URL = try? await fetchMediaUseCase.execute(media: media, in: bookID) else { return } + output.send(.mediaLoadedWithURL(media: media, url: mediaURL)) + } + private func converTextToPage(text: NSAttributedString) -> Page { + let (savedText, metadata) = separateStorageInformation(text) + let newPage = Page( + id: page.id, + metadata: metadata, + text: savedText + ) + return newPage + } + /// NSAttributedString에서 Text와 Attachment 정보를 추출해냅니다. + private func separateStorageInformation( + _ text: NSAttributedString + ) -> (String, [Int: MediaDescription]) { + var metaData = [Int: MediaDescription]() + let mutableAttributedString = NSMutableAttributedString(attributedString: text) + + text.enumerateAttribute(.attachment, in: NSRange(location: 0, length: text.length)) { value, range, _ in + if let mediaAttachment = value as? MediaAttachment { + // 위치와 URL 저장 + metaData[range.location] = mediaAttachment.mediaDescription + // Placeholder로 텍스트 대체 + mutableAttributedString.replaceCharacters(in: range, with: " ") + } + } + return (mutableAttributedString.string, metaData) + } +} From 218cba7281fac0918ff12d92d8e1e4d1eed76857 Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:38:05 +0900 Subject: [PATCH 14/60] =?UTF-8?q?feat:=20EditBookViewModelFactory=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/EditBookViewModelFactory.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift new file mode 100644 index 00000000..4280fd42 --- /dev/null +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift @@ -0,0 +1,37 @@ +import MHDomain + +public struct EditBookViewModelFactory { + private let fetchBookUseCase: FetchBookUseCase + private let updateBookUseCase: UpdateBookUseCase + private let storeBookUseCase: PersistentlyStoreMediaUseCase + private let createMediaUseCase: CreateMediaUseCase + private let fetchMediaUseCase: FetchMediaUseCase + private let deleteMediaUseCase: DeleteMediaUseCase + + public init( + fetchBookUseCase: FetchBookUseCase, + updateBookUseCase: UpdateBookUseCase, + storeBookUseCase: PersistentlyStoreMediaUseCase, + createMediaUseCase: CreateMediaUseCase, + fetchMediaUseCase: FetchMediaUseCase, + deleteMediaUseCase: DeleteMediaUseCase + ) { + self.fetchBookUseCase = fetchBookUseCase + self.updateBookUseCase = updateBookUseCase + self.storeBookUseCase = storeBookUseCase + self.createMediaUseCase = createMediaUseCase + self.fetchMediaUseCase = fetchMediaUseCase + self.deleteMediaUseCase = deleteMediaUseCase + } + + func make() -> EditBookViewModel { + EditBookViewModel( + fetchBookUseCase: fetchBookUseCase, + updateBookUseCase: updateBookUseCase, + storeBookUseCase: storeBookUseCase, + createMediaUseCase: createMediaUseCase, + fetchMediaUseCase: fetchMediaUseCase, + deleteMediaUseCase: deleteMediaUseCase + ) + } +} From e3c1865190e9f44963693759d1dd2b18b4d357e1 Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:38:56 +0900 Subject: [PATCH 15/60] =?UTF-8?q?feat:=20EditBook=EA=B3=BC=20ViewModel=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditBook/View/EditBookViewController.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index aa937236..a5156318 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -1,5 +1,6 @@ import UIKit -import Combine +import MHCore +@preconcurrency import Combine final class EditBookViewController: UIViewController { // MARK: - Constant @@ -67,6 +68,7 @@ final class EditBookViewController: UIViewController { private let viewModel: EditBookViewModel private let input = PassthroughSubject() private var cancellables = Set() + var id: UUID? // TODO: - 지워야함 // MARK: - Initializer init(viewModel: EditBookViewModel) { @@ -75,8 +77,8 @@ final class EditBookViewController: UIViewController { super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { - self.viewModel = EditBookViewModel() - + guard let viewModel = try? DIContainer.shared.resolve(EditBookViewModelFactory.self) else { return nil } + self.viewModel = viewModel.make() super.init(coder: coder) } @@ -92,6 +94,8 @@ final class EditBookViewController: UIViewController { configureKeyboard() configureBinding() configureButtonAction() + guard let bookID = id else { return } + input.send(.viewDidLoad(bookID: bookID)) } // MARK: - Setup & Configuration @@ -229,6 +233,7 @@ final class EditBookViewController: UIViewController { // TODO: - 로직을 정한다음에 Action 추가 let addImageAction = UIAction { [weak self] _ in // TODO: - 이미지 추가 로직 + self?.input.send(.didAddMediaWithData(type: .image, atPage: 0, data: UIImage(resource: .bookMake).pngData()!)) } addImageButton.addAction(addImageAction, for: .touchUpInside) @@ -287,7 +292,7 @@ extension EditBookViewController: UITableViewDelegate { // MARK: - UITableViewDataSource extension EditBookViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 10 // TODO: - 추후 더미데이터 대신 뷰모델 데이터로 변경 + return viewModel.numberOfPages() } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell( @@ -295,6 +300,8 @@ extension EditBookViewController: UITableViewDataSource { for: indexPath ) as? EditPageCell else { return UITableViewCell() } + cell.configure(viewModel: viewModel.page(at: indexPath.row)) + return cell } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { From a50363d4ed0f820006ee89c7b5367837a772b358 Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:39:22 +0900 Subject: [PATCH 16/60] =?UTF-8?q?feat:=20Cell=EA=B3=BC=20ViewModel?= =?UTF-8?q?=EC=97=B0=EA=B2=B0,=20ContentView=EA=B8=B0=EC=A4=80=20=EB=B7=B0?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 210 ++++++++++-------- 1 file changed, 118 insertions(+), 92 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 2fc9557e..04bf9375 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -1,7 +1,7 @@ import UIKit +import Combine import MHDomain import MHCore -import MHData final class EditPageCell: UITableViewCell { // MARK: - Property @@ -22,6 +22,9 @@ final class EditPageCell: UITableViewCell { return textView }() private var textStorage: NSTextStorage? + private var viewModel: EditPageViewModel? + private let input = PassthroughSubject() + private var cancellables = Set() // MARK: - Initializer override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -31,7 +34,6 @@ final class EditPageCell: UITableViewCell { configureAddSubView() configureConstraints() } - required init?(coder: NSCoder) { super.init(coder: coder) @@ -40,6 +42,14 @@ final class EditPageCell: UITableViewCell { configureConstraints() } + // MARK: - LifeCycle + override func prepareForReuse() { + super.prepareForReuse() + + saveContents() + cancellables.removeAll() + } + // MARK: - Setup & Configuration private func setup() { backgroundColor = .clear @@ -48,114 +58,130 @@ final class EditPageCell: UITableViewCell { textStorage = textView.textStorage } private func configureAddSubView() { - addSubview(textView) + contentView.addSubview(textView) } private func configureConstraints() { textView.setAnchor( - top: topAnchor, constantTop: 10, - leading: leadingAnchor, constantLeading: 10, - bottom: bottomAnchor, constantBottom: 10, - trailing: trailingAnchor, constantTrailing: 10 + top: contentView.topAnchor, constantTop: 10, + leading: contentView.leadingAnchor, constantLeading: 10, + bottom: contentView.bottomAnchor, constantBottom: 10, + trailing: contentView.trailingAnchor, constantTrailing: 10 ) } - - // MARK: - TouchEvent - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - textView.becomeFirstResponder() - - super.touchesBegan(touches, with: event) + private func configureBinding() { + let output = viewModel?.transform(input: input.eraseToAnyPublisher()) + output? + .receive(on: DispatchQueue.main) + .sink { [weak self] event in + switch event { + case .page(let page): + self?.configurePage(page: page) + case let .mediaAddedWithData(media, data): + self?.mediaAddedWithData(media: media, data: data) + case let .mediaAddedWithURL(media, url): + self?.mediaAddedWithURL(media: media, url: url) + case let .mediaLoadedWithData(media, data): + self?.mediaLoadedWithData(media: media, data: data) + case let .mediaLoadedWithURL(media, url): + self?.mediaLoadedWithURL(media: media, url: url) + } + }.store(in: &cancellables) } // MARK: - Method - /// Storage에서 Text와 Attachment 정보를 추출해냅니다. - private func separateStorageInformation( - _ textStorage: NSTextStorage - ) -> (String, [Int: MediaDescription]) { - var metaData = [Int: MediaDescription]() - let mutableAttributedString = NSMutableAttributedString(attributedString: textStorage) - - textStorage.enumerateAttribute(.attachment, in: NSRange(location: 0, length: textStorage.length)) { value, range, _ in - if let mediaAttachment = value as? MediaAttachment { - // 위치와 URL 저장 - metaData[range.location] = mediaAttachment.mediaDescription - // Placeholder로 텍스트 대체 - mutableAttributedString.replaceCharacters(in: range, with: " ") + func configure(viewModel: EditPageViewModel) { + self.viewModel = viewModel + configureBinding() + input.send(.pageWillAppear) + } + + // MARK: - Helper + private func configurePage(page: Page) { + let mergedText = mergeStorageInformation( + text: page.text, + attachmentMetaData: page.metadata + ) + textStorage?.setAttributedString(mergedText) + } + private func mediaAddedWithData(media: MediaDescription, data: Data) { + let attachment = MediaAttachment( + view: MHPolaroidPhotoView(), + description: media + ) + attachment.configure(with: data) + let text = NSMutableAttributedString(attachment: attachment) + text.addAttributes([.font: UIFont.ownglyphBerry(size: 20), + .foregroundColor: UIColor.mhTitle], + range: NSRange(location: 0, length: 1)) + textStorage?.append(text) + } + private func mediaAddedWithURL(media: MediaDescription, url: URL) { + let attachment = MediaAttachment( + view: MHPolaroidPhotoView(), + description: media + ) + attachment.configure(with: url) + textStorage?.append(NSAttributedString(attachment: attachment)) + textView.font = .ownglyphBerry(size: 20) + } + private func mediaLoadedWithData(media: MediaDescription, data: Data) { + let attachment = findAttachment(by: media) + attachment?.configure(with: data) + } + private func mediaLoadedWithURL(media: MediaDescription, url: URL) { + let attachment = findAttachment(by: media) + attachment?.configure(with: url) + } + private func saveContents() { + guard let textStorage else { return } + let range = NSRange(location: 0, length: textStorage.length) + let text = textStorage.attributedSubstring(from: range) + input.send(.pageWillDisappear(attributedText: text)) + } + /// Text에서 특정 Attachment를 찾아서 적용합니다. + private func findAttachment( + by media: MediaDescription + ) -> MediaAttachment? { + var attachment: MediaAttachment? + guard let textStorage else { return attachment } + textStorage + .enumerateAttribute( + .attachment, + in: NSRange(location: 0, length: textStorage.length) + ) { value, _, _ in + if let mediaAttachment = value as? MediaAttachment, + mediaAttachment.mediaDescription.id == media.id { + attachment = mediaAttachment + return } } - return (mutableAttributedString.string, metaData) + return attachment } /// Text와 Attachment 정보를 하나의 문자열로 조합합니다. private func mergeStorageInformation( - text savedAttributedString: NSAttributedString, + text: String, attachmentMetaData: [Int: MediaDescription] ) -> NSAttributedString { - let mutableAttributedString = NSMutableAttributedString(attributedString: savedAttributedString) - - attachmentMetaData - .forEach { - location, - description in - let range = NSRange(location: location, length: 1) - let mediaAttachment = MediaAttachment( - view: MHPolaroidPhotoView(), // TODO: - 이거 바꿔줘야함... - description: description - ) - mediaAttachment.mediaDescription = description - let attachmentString = NSAttributedString(attachment: mediaAttachment) - - // Placeholder(공백) 교체 - mutableAttributedString.replaceCharacters(in: range, with: attachmentString) - } - - return mutableAttributedString - } - private func saveTextWithAttachments() { - guard let textStorage = textStorage else { return } - - let documentDirectory = FileManager.default.urls( - for: .documentDirectory, - in: .userDomainMask - ).first! - let (savedText, metadata) = separateStorageInformation(textStorage) - - do { - MHFileManager().create(at: <#T##String#>, fileName: <#T##String#>, data: <#T##Data#>) - MHLogger.debug("Text with attachments saved to \(fileURL)") - } catch { - MHLogger.error("Error saving text with attachments: \(error)") - } - - do { - let metadataFileURL = documentDirectory.appendingPathComponent("attachmentMetadata.json") - let metadataData = try JSONSerialization.data(withJSONObject: metadata, options: []) - try metadataData.write(to: metadataFileURL) - MHLogger.debug("Attachment metadata saved to \(metadataFileURL)") - } catch { - MHLogger.error("Error saving attachment metadata: \(error)") - } - } - private func loadTextWithAttachments() { - let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let fileURL = documentDirectory.appendingPathComponent("textWithAttachments.rtf") - do { - let rtfData = try Data(contentsOf: fileURL) - let savedText = try NSAttributedString( - data: rtfData, - options: [.documentType: NSAttributedString.DocumentType.rtf], - documentAttributes: nil + let mutableAttributedString = NSMutableAttributedString(string: text) + attachmentMetaData.forEach { location, description in + let range = NSRange(location: location, length: 1) + let mediaAttachment = MediaAttachment( + view: MHPolaroidPhotoView(), // TODO: - 이거 바꿔줘야함... + description: description ) - let metadataFileURL = documentDirectory.appendingPathComponent("attachmentMetadata.json") - let metadataData = try Data(contentsOf: metadataFileURL) - let metadata = try JSONSerialization.jsonObject(with: metadataData) as? [String: Any] - // 커스텀 NSTextAttachment로 변환 - let restoredText = mergeStorageInformation(text: savedText, attachmentMetaData: metadata) - textStorage?.setAttributedString(restoredText) - MHLogger.debug("Text with attachments loaded from \(fileURL)") - } catch { - MHLogger.error("Error loading text with attachments: \(error)") + input.send(.didRequestMediaDataForData(media: description)) + let attachmentString = NSAttributedString(attachment: mediaAttachment) + // Placeholder(공백) 교체 + mutableAttributedString.replaceCharacters(in: range, with: attachmentString) } + + return mutableAttributedString } - private func isAcceptableHight(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText attributedText: NSAttributedString) -> Bool { + private func isAcceptableHight( + _ textView: UITextView, + shouldChangeTextIn range: NSRange, + replacementText attributedText: NSAttributedString + ) -> Bool { let updatedText = NSMutableAttributedString(attributedString: textView.attributedText) let textViewWidth = textView.bounds.width let temporaryTextView = UITextView( From 533a4577bfad2d393184051a69bf918dd976cab3 Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:40:02 +0900 Subject: [PATCH 17/60] =?UTF-8?q?feat:=20PolaroidView=EA=B0=80=20=EB=B0=9B?= =?UTF-8?q?=EC=9D=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/Design/MHPolaroidPhotoView.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHPolaroidPhotoView.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHPolaroidPhotoView.swift index a2e37f03..b2a7ca12 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHPolaroidPhotoView.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Design/MHPolaroidPhotoView.swift @@ -81,14 +81,10 @@ final class MHPolaroidPhotoView: UIView { } extension MHPolaroidPhotoView: @preconcurrency MediaAttachable { - // TODO: - 임시로 넣어 놓음 일단 - var mediaType: MediaType { - get { - .image - } + func configureSource(with mediaDescription: MediaDescription, data: Data) { + photoImageView.image = UIImage(data: data) } - func configureSource(with path: URL?) { - guard let path else { return } - photoImageView.image = UIImage(contentsOfFile: path.absoluteString) + func configureSource(with mediaDescription: MediaDescription, url: URL) { + photoImageView.image = UIImage(contentsOfFile: url.path) } } From b13a00528fc4b2813c3ad9e34fcf4d2561c8197b Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:40:50 +0900 Subject: [PATCH 18/60] =?UTF-8?q?refactor:=20MediaAttachment=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditBook/View/MediaAttachment.swift | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift index ff660c1d..42af17d7 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift @@ -9,11 +9,7 @@ protocol MediaAttachmentDataSource: AnyObject { final class MediaAttachment: NSTextAttachment { // MARK: - Property private let view: (UIView & MediaAttachable) - var mediaDescription: MediaDescription { - didSet { - view.configureSource(with: mediaDescription) - } - } + let mediaDescription: MediaDescription weak var dataSource: MediaAttachmentDataSource? // MARK: - Initializer @@ -28,7 +24,11 @@ final class MediaAttachment: NSTextAttachment { } // MARK: - ViewConfigures - override func viewProvider(for parentView: UIView?, location: any NSTextLocation, textContainer: NSTextContainer?) -> NSTextAttachmentViewProvider? { + override func viewProvider( + for parentView: UIView?, + location: any NSTextLocation, + textContainer: NSTextContainer? + ) -> NSTextAttachmentViewProvider? { let provider = MediaAttachmentViewProvider( textAttachment: self, parentView: parentView, @@ -40,13 +40,20 @@ final class MediaAttachment: NSTextAttachment { provider.type = mediaDescription.type return provider } - override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? { + override func image( + forBounds imageBounds: CGRect, + textContainer: NSTextContainer?, + characterIndex charIndex: Int + ) -> UIImage? { return dataSource?.mediaAttachmentDragingImage(self, about: view) } // MARK: - Method - func setup() { - view.configureSource(with: mediaDescription) + func configure(with data: Data) { + view.configureSource(with: mediaDescription, data: data) + } + func configure(with url: URL) { + view.configureSource(with: mediaDescription, url: url) } } @@ -81,5 +88,6 @@ class MediaAttachmentViewProvider: NSTextAttachmentViewProvider { // MARK: - MediaAttachable protocol MediaAttachable { - func configureSource(with description: MediaDescription) + func configureSource(with mediaDescription: MediaDescription, data: Data) + func configureSource(with mediaDescription: MediaDescription, url: URL) } From 84c82205a0b32607269f585d429272ae4edfb90a Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:59:32 +0900 Subject: [PATCH 19/60] =?UTF-8?q?refactor:=20default=20attribute=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 04bf9375..c7f2dd8b 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -4,6 +4,11 @@ import MHDomain import MHCore final class EditPageCell: UITableViewCell { + // MARK: - Constant + private let defaultAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.ownglyphBerry(size: 20), + .foregroundColor: UIColor.mhTitle + ] // MARK: - Property private let textView: UITextView = { let textView = UITextView() @@ -110,8 +115,7 @@ final class EditPageCell: UITableViewCell { ) attachment.configure(with: data) let text = NSMutableAttributedString(attachment: attachment) - text.addAttributes([.font: UIFont.ownglyphBerry(size: 20), - .foregroundColor: UIColor.mhTitle], + text.addAttributes(defaultAttributes, range: NSRange(location: 0, length: 1)) textStorage?.append(text) } @@ -121,8 +125,10 @@ final class EditPageCell: UITableViewCell { description: media ) attachment.configure(with: url) - textStorage?.append(NSAttributedString(attachment: attachment)) - textView.font = .ownglyphBerry(size: 20) + let text = NSMutableAttributedString(attachment: attachment) + text.addAttributes(defaultAttributes, + range: NSRange(location: 0, length: 1)) + textStorage?.append(text) } private func mediaLoadedWithData(media: MediaDescription, data: Data) { let attachment = findAttachment(by: media) @@ -175,6 +181,9 @@ final class EditPageCell: UITableViewCell { mutableAttributedString.replaceCharacters(in: range, with: attachmentString) } + mutableAttributedString.addAttributes(defaultAttributes, + range: NSRange(location: 0, length: mutableAttributedString.length)) + return mutableAttributedString } private func isAcceptableHight( @@ -187,7 +196,6 @@ final class EditPageCell: UITableViewCell { let temporaryTextView = UITextView( frame: CGRect(x: 0, y: 0, width: textViewWidth, height: .greatestFiniteMagnitude) ) - updatedText.append(attributedText) updatedText.replaceCharacters(in: range, with: attributedText) temporaryTextView.attributedText = updatedText temporaryTextView.sizeToFit() @@ -207,6 +215,10 @@ extension EditPageCell: @preconcurrency MediaAttachmentDataSource { // MARK: - UITextViewDelegate extension EditPageCell: UITextViewDelegate { func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - return isAcceptableHight(textView, shouldChangeTextIn: range, replacementText: .init(string: text)) + let attributedText = NSAttributedString( + string: text, + attributes: defaultAttributes + ) + return isAcceptableHight(textView, shouldChangeTextIn: range, replacementText: attributedText) } } From 40438b536c18cd285cb27b65e1ec0542a3dabf0f Mon Sep 17 00:00:00 2001 From: iceHood Date: Fri, 29 Nov 2024 20:59:40 +0900 Subject: [PATCH 20/60] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/ViewModel/EditPageViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift index 980da11a..15520659 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift @@ -103,7 +103,6 @@ final class EditPageViewModel: ViewModelType { text.enumerateAttribute(.attachment, in: NSRange(location: 0, length: text.length)) { value, range, _ in if let mediaAttachment = value as? MediaAttachment { - // 위치와 URL 저장 metaData[range.location] = mediaAttachment.mediaDescription // Placeholder로 텍스트 대체 mutableAttributedString.replaceCharacters(in: range, with: " ") From 0aa8f677ec61f6b9869cea8683ae20f68acd140e Mon Sep 17 00:00:00 2001 From: iceHood Date: Sat, 30 Nov 2024 13:07:20 +0900 Subject: [PATCH 21/60] =?UTF-8?q?refactor:=20TODO=EC=9E=91=EC=84=B1,=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9C=84=EC=B9=98=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/ViewModel/EditPageViewModel.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift index 15520659..74682f9a 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift @@ -68,9 +68,15 @@ final class EditPageViewModel: ViewModelType { self.page = page } private func loadMediaForData(media: MediaDescription) async { + // TODO: - Loading실패시 로딩실패 처리 guard let mediaData: Data = try? await fetchMediaUseCase.execute(media: media, in: bookID) else { return } output.send(.mediaLoadedWithData(media: media, data: mediaData)) } + private func loadMediaForURL(media: MediaDescription) async { + // TODO: - Loading실패시 로딩실패 처리 + guard let mediaURL: URL = try? await fetchMediaUseCase.execute(media: media, in: bookID) else { return } + output.send(.mediaLoadedWithURL(media: media, url: mediaURL)) + } // MARK: - Method func addMedia(media: MediaDescription, data: Data) { @@ -81,10 +87,6 @@ final class EditPageViewModel: ViewModelType { } // MARK: - Helper - private func loadMediaForURL(media: MediaDescription) async { - guard let mediaURL: URL = try? await fetchMediaUseCase.execute(media: media, in: bookID) else { return } - output.send(.mediaLoadedWithURL(media: media, url: mediaURL)) - } private func converTextToPage(text: NSAttributedString) -> Page { let (savedText, metadata) = separateStorageInformation(text) let newPage = Page( From 230d7bce0c2698591da17cf52460bd93bc81aed4 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sat, 30 Nov 2024 13:07:37 +0900 Subject: [PATCH 22/60] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/ViewModel/EditBookViewModel.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift index 8add122c..fc210313 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift @@ -12,7 +12,7 @@ final class EditBookViewModel: ViewModelType { case didSaveButtonTapped } enum Output { - case setTableView + case updateTableView } // MARK: - Property @@ -74,7 +74,7 @@ final class EditBookViewModel: ViewModelType { ) pageViewModels.append(pageViewModel) } - output.send(.setTableView) + output.send(.updateTableView) } private func addMedia(type: MediaType, at index: Int, with data: Data) { let description = MediaDescription( @@ -92,6 +92,10 @@ final class EditBookViewModel: ViewModelType { } private func saveMediaAll() async { guard let bookID else { return } + pageViewModels.map { viewModel in + + } + try? await updateBookUseCase.execute(book: <#T##Book#>) try? await storeBookUseCase.execute(to: bookID) } From 1ff40f219037a6f0b55830c66b1e24ed7530d735 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sat, 30 Nov 2024 13:08:05 +0900 Subject: [PATCH 23/60] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/EditBookViewController.swift | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index a5156318..d71543ec 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -136,11 +136,11 @@ final class EditBookViewController: UIViewController { // 네비게이션 오른쪽 아이템 navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "저장", + title: "기록", normal: normalAttributes, selected: selectedAttributes ) { [weak self] in - // TODO: - 저장하는 로직 + self?.input.send(.didSaveButtonTapped) self?.navigationController?.popViewController(animated: true) } @@ -223,7 +223,7 @@ final class EditBookViewController: UIViewController { output.receive(on: DispatchQueue.main) .sink { [weak self] event in switch event { - case .setTableView: + case .updateTableView: self?.editPageTableView.reloadData() } } @@ -232,8 +232,11 @@ final class EditBookViewController: UIViewController { private func configureButtonAction() { // TODO: - 로직을 정한다음에 Action 추가 let addImageAction = UIAction { [weak self] _ in - // TODO: - 이미지 추가 로직 - self?.input.send(.didAddMediaWithData(type: .image, atPage: 0, data: UIImage(resource: .bookMake).pngData()!)) + // TODO: - 이미지 받는 임시 로직 + guard let data = UIImage(resource: .bookMake).pngData(), + let currentPage = self?.editPageTableView.indexPathForSelectedRow?.row + else { return } + self?.input.send(.didAddMediaWithData(type: .image, atPage: currentPage, data: data)) } addImageButton.addAction(addImageAction, for: .touchUpInside) @@ -242,18 +245,13 @@ final class EditBookViewController: UIViewController { } addVideoButton.addAction(addVideoAction, for: .touchUpInside) - let addTextAction = UIAction { [weak self] _ in - // TODO: - 텍스트 추가로직??? - } - addTextButton.addAction(addTextAction, for: .touchUpInside) - let addAudioAction = UIAction { [weak self] _ in // TODO: - 오디오 추가 로직 } addAudioButton.addAction(addAudioAction, for: .touchUpInside) let publishAction = UIAction { [weak self] _ in - // TODO: - 발행 로직 + self?.input.send(.didSaveButtonTapped) self?.navigationController?.popViewController(animated: true) } publishButton.addAction(publishAction, for: .touchUpInside) From 667b01c27130c58d7bc205c2abbff9dc9d1d765a Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 11:41:26 +0900 Subject: [PATCH 24/60] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=EC=9C=A0?= =?UTF-8?q?=ED=98=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MemorialHouse/MHCore/MHCore/MHDataError.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MemorialHouse/MHCore/MHCore/MHDataError.swift b/MemorialHouse/MHCore/MHCore/MHDataError.swift index 801004c3..87fa4f24 100644 --- a/MemorialHouse/MHCore/MHCore/MHDataError.swift +++ b/MemorialHouse/MHCore/MHCore/MHDataError.swift @@ -15,6 +15,8 @@ public enum MHDataError: Error, CustomStringConvertible, Equatable { case fileDeletionFailure case fileMovingFailure case fileNotExists + case snapshotEncodingFailure + case snapshotDecodingFailure case generalFailure public var description: String { @@ -47,6 +49,10 @@ public enum MHDataError: Error, CustomStringConvertible, Equatable { "파일 이동 실패" case .fileNotExists: "파일이 존재하지 않습니다" + case .snapshotEncodingFailure: + "Snapshot 인코딩 실패" + case .snapshotDecodingFailure: + "Snapshot 디코딩 실패" case .generalFailure: "알 수 없는 에러입니다." } From 56ddcf168cc72e83cc4618b4bfcd4a1365375ff7 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 11:42:53 +0900 Subject: [PATCH 25/60] =?UTF-8?q?feat:=20Snapshot=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=8C=8C=EC=9D=BC=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FileManager/MHFileManager.swift | 16 +++- .../MHData/LocalStorage/FileStorage.swift | 8 ++ .../Repository/LocalMediaRepository.swift | 73 +++++++++++++++---- .../MHDomain/MHDomain/Entity/MediaType.swift | 10 ++- .../MHDomain/Repository/MediaRepository.swift | 4 + .../UseCase/DefaultMediaUseCase.swift | 12 ++- .../UseCase/Interface/MediaUseCase.swift | 6 +- 7 files changed, 103 insertions(+), 26 deletions(-) diff --git a/MemorialHouse/MHData/MHData/LocalStorage/FileManager/MHFileManager.swift b/MemorialHouse/MHData/MHData/LocalStorage/FileManager/MHFileManager.swift index cf2fdc78..31a11c93 100644 --- a/MemorialHouse/MHData/MHData/LocalStorage/FileManager/MHFileManager.swift +++ b/MemorialHouse/MHData/MHData/LocalStorage/FileManager/MHFileManager.swift @@ -22,7 +22,7 @@ extension MHFileManager: FileStorage { do { try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) - try data.write(to: dataPath) + try data.write(to: dataPath, options: .atomic) return .success(()) } catch { return .failure(.fileCreationFailure) @@ -185,5 +185,19 @@ extension MHFileManager: FileStorage { return .success(originDataPath) } + public func getFileNames(at path: String) async -> Result<[String], MHDataError> { + guard let originDirectory = fileManager.urls( + for: directoryType, + in: .userDomainMask + ).first?.appending(path: path) + else { return .failure(.directorySettingFailure) } + + do { + let files = try fileManager.contentsOfDirectory(atPath: originDirectory.path) + return .success(files) + } catch { + return .failure(.fileNotExists) + } + } } diff --git a/MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift b/MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift index 4a3d6296..dd981155 100644 --- a/MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift +++ b/MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift @@ -69,4 +69,12 @@ public protocol FileStorage: Sendable { /// - name: Documents/{path}/{name} 이 파일 URL을 반환합니다. (확장자 명시 필요) /// - Returns: 파일 URL을 반환합니다. func getURL(at path: String, fileName name: String) async -> Result + + /// 지정된 경로의 파일 목록을 반환합니다. + /// Documents폴더를 기준으로 파일 이름 목록을 반환합니다. + /// path는 디렉토리여야 합니다. + /// - Parameters: + /// - path: Documents/{path} 이런식으로 들어갑니다 + /// - Returns: 파일 이름 목록을 반환합니다 + func getFileNames(at path: String) async -> Result<[String], MHDataError> } diff --git a/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift b/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift index 7da25579..00861760 100644 --- a/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift +++ b/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift @@ -6,60 +6,103 @@ import AVFoundation public struct LocalMediaRepository: MediaRepository, Sendable { private let storage: FileStorage + private let temporaryPath = "temp" + private let snapshotFileName = ".snapshot" public init(storage: FileStorage) { self.storage = storage } - public func create(media mediaDescription: MediaDescription, data: Data, to bookID: UUID?) async -> Result { + public func create( + media mediaDescription: MediaDescription, + data: Data, + to bookID: UUID? + ) async -> Result { let path = bookID == nil - ? "temp" + ? temporaryPath : bookID!.uuidString - let fileName = mediaDescription.id.uuidString + let fileName = fileName(of: mediaDescription) return await storage.create(at: path, fileName: fileName, data: data) } - public func create(media mediaDescription: MediaDescription, from: URL, to bookID: UUID?) async -> Result { + public func create( + media mediaDescription: MediaDescription, + from: URL, + to bookID: UUID? + ) async -> Result { let path = bookID == nil - ? "temp" + ? temporaryPath : bookID!.uuidString - let fileName = mediaDescription.id.uuidString + let fileName = fileName(of: mediaDescription) return await storage.copy(at: from, to: path, newFileName: fileName) } public func read(media mediaDescription: MediaDescription, from bookID: UUID?) async -> Result { let path = bookID == nil - ? "temp" + ? temporaryPath : bookID!.uuidString - let fileName = mediaDescription.id.uuidString + let fileName = fileName(of: mediaDescription) return await storage.read(at: path, fileName: fileName) } public func getURL(media mediaDescription: MediaDescription, from bookID: UUID?) async -> Result { let path = bookID == nil - ? "temp" + ? temporaryPath : bookID!.uuidString - let fileName = mediaDescription.id.uuidString + let fileName = fileName(of: mediaDescription) return await storage.getURL(at: path, fileName: fileName) } public func delete(media mediaDescription: MediaDescription, at bookID: UUID?) async -> Result { let path = bookID == nil - ? "temp" + ? temporaryPath : bookID!.uuidString - let fileName = mediaDescription.id.uuidString + let fileName = fileName(of: mediaDescription) return await storage.delete(at: path, fileName: fileName) } public func moveTemporaryMedia(_ mediaDescription: MediaDescription, to bookID: UUID) async -> Result { let path = bookID.uuidString - let fileName = mediaDescription.id.uuidString + let fileName = fileName(of: mediaDescription) - return await storage.move(at: "temp", fileName: fileName, to: path) + return await storage.move(at: temporaryPath, fileName: fileName, to: path) } public func moveAllTemporaryMedia(to bookID: UUID) async -> Result { let path = bookID.uuidString - return await storage.moveAll(in: "temp", to: path) + return await storage.moveAll(in: temporaryPath, to: path) + } + + // MARK: - Snpashot + public func createSnapshot(for media: [MediaDescription], in bookID: UUID) async -> Result { + let path = bookID.uuidString + let mediaList = media.map { fileName(of: $0) } + guard let snapshot = try? JSONEncoder().encode(mediaList) + else { return .failure(.snapshotEncodingFailure) } + + return await storage.create(at: path, fileName: snapshotFileName, data: snapshot) + } + public func deleteMediaBySnapshot(for bookID: UUID) async -> Result { + let path = bookID.uuidString + + do { + let snapshotData = try await storage.read(at: path, fileName: snapshotFileName).get() + let mediaSet = Set(try JSONDecoder().decode([String].self, from: snapshotData)) + // snapshot 파일은 제외 + let currentFiles = Set(try await storage.getFileNames(at: path).get()).subtracting([snapshotFileName]) + let shouldDelete = currentFiles.subtracting(mediaSet) + for fileName in shouldDelete { + _ = try await storage.delete(at: path, fileName: fileName).get() + } + return .success(()) + } catch let error as MHDataError { + return .failure(error) + } catch { + return .failure(.generalFailure) + } + } + // MARK: - Helper + private func fileName(of media: MediaDescription) -> String { + return media.id.uuidString + media.type.defaultFileExtension } } diff --git a/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift b/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift index 6aff8c17..3432c90d 100644 --- a/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift +++ b/MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift @@ -3,14 +3,16 @@ public enum MediaType: String, Sendable { case video case audio - var defaultFileExtension: String { + /// 기본 파일 확장자를 반환합니다. + /// 사진은 .png, 비디오는 .mp4, 오디오는 .m4a를 반환합니다. + public var defaultFileExtension: String { switch self { case .image: - return "png" + return ".png" case .video: - return "mp4" + return ".mp4" case .audio: - return "m4a" + return ".m4a" } } } diff --git a/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift b/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift index 65ff7b3c..25809869 100644 --- a/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift +++ b/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift @@ -10,4 +10,8 @@ public protocol MediaRepository: Sendable { func delete(media mediaDescription: MediaDescription, at bookID: UUID?) async -> Result func moveTemporaryMedia(_ mediaDescription: MediaDescription, to bookID: UUID) async -> Result func moveAllTemporaryMedia(to bookID: UUID) async -> Result + + // MARK: - Snapshot + func createSnapshot(for media: [MediaDescription], in bookID: UUID) async -> Result + func deleteMediaBySnapshot(for bookID: UUID) async -> Result } diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift index a4038763..af69fe70 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift @@ -11,11 +11,11 @@ public struct DefaultCreateMediaUseCase: CreateMediaUseCase, Sendable { } // MARK: - Method - public func execute(media: MediaDescription, data: Data) async throws { - try await repository.create(media: media, data: data, to: nil).get() + public func execute(media: MediaDescription, data: Data, at bookID: UUID?) async throws { + try await repository.create(media: media, data: data, to: bookID).get() } - public func execute(media: MediaDescription, in url: URL) async throws { - try await repository.create(media: media, from: url, to: nil).get() + public func execute(media: MediaDescription, from url: URL, at bookID: UUID?) async throws { + try await repository.create(media: media, from: url, to: bookID).get() } } @@ -77,4 +77,8 @@ public struct DefaultPersistentlyStoreMediaUseCase: PersistentlyStoreMediaUseCas public func execute(to bookID: UUID) async throws { try await repository.moveAllTemporaryMedia(to: bookID).get() } + public func execute(to bookID: UUID, mediaList: [MediaDescription]) async throws { + try await repository.createSnapshot(for: mediaList, in: bookID).get() + try await repository.deleteMediaBySnapshot(for: bookID).get() + } } diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift index eaaa009a..68a73098 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift @@ -1,8 +1,8 @@ import MHFoundation public protocol CreateMediaUseCase: Sendable { - func execute(media: MediaDescription, data: Data) async throws - func execute(media: MediaDescription, in url: URL) async throws + func execute(media: MediaDescription, data: Data, at bookID: UUID?) async throws + func execute(media: MediaDescription, from url: URL, at bookID: UUID?) async throws } public protocol FetchMediaUseCase: Sendable { @@ -15,5 +15,7 @@ public protocol DeleteMediaUseCase: Sendable { } public protocol PersistentlyStoreMediaUseCase: Sendable { + @available(*, deprecated, message: "temp를 더이상 사용하지 않습니다.") func execute(to bookID: UUID) async throws + func execute(to bookID: UUID, mediaList: [MediaDescription]) async throws } From 3bb9487ee127fb434dfb8095ece384a981e19a49 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 11:45:07 +0900 Subject: [PATCH 26/60] =?UTF-8?q?feat:=20storeMedia=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EA=B5=AC=EC=B2=B4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHApplication/Source/App/SceneDelegate.swift | 5 ++--- .../EditBook/ViewModel/EditBookViewModelFactory.swift | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift index 44dbdaca..f5976693 100644 --- a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift +++ b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift @@ -209,9 +209,8 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) // MARK: - EditBook ViewModel - let fetchBookUseCase = try DIContainer.shared.resolve(FetchBookUseCase.self) let updateBookUseCase = try DIContainer.shared.resolve(UpdateBookUseCase.self) - let storeBookUseCase = try DIContainer.shared.resolve(PersistentlyStoreMediaUseCase.self) + let storeMediaUseCase = try DIContainer.shared.resolve(PersistentlyStoreMediaUseCase.self) let createMediaUseCase = try DIContainer.shared.resolve(CreateMediaUseCase.self) let fetchMediaUseCase = try DIContainer.shared.resolve(FetchMediaUseCase.self) let deleteMediaUseCase = try DIContainer.shared.resolve(DeleteMediaUseCase.self) @@ -220,7 +219,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { object: EditBookViewModelFactory( fetchBookUseCase: fetchBookUseCase, updateBookUseCase: updateBookUseCase, - storeBookUseCase: storeBookUseCase, + storeMediaUseCase: storeMediaUseCase, createMediaUseCase: createMediaUseCase, fetchMediaUseCase: fetchMediaUseCase, deleteMediaUseCase: deleteMediaUseCase diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift index 4280fd42..6d6f7468 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift @@ -3,7 +3,7 @@ import MHDomain public struct EditBookViewModelFactory { private let fetchBookUseCase: FetchBookUseCase private let updateBookUseCase: UpdateBookUseCase - private let storeBookUseCase: PersistentlyStoreMediaUseCase + private let storeMediaUseCase: PersistentlyStoreMediaUseCase private let createMediaUseCase: CreateMediaUseCase private let fetchMediaUseCase: FetchMediaUseCase private let deleteMediaUseCase: DeleteMediaUseCase @@ -11,14 +11,14 @@ public struct EditBookViewModelFactory { public init( fetchBookUseCase: FetchBookUseCase, updateBookUseCase: UpdateBookUseCase, - storeBookUseCase: PersistentlyStoreMediaUseCase, + storeMediaUseCase: PersistentlyStoreMediaUseCase, createMediaUseCase: CreateMediaUseCase, fetchMediaUseCase: FetchMediaUseCase, deleteMediaUseCase: DeleteMediaUseCase ) { self.fetchBookUseCase = fetchBookUseCase self.updateBookUseCase = updateBookUseCase - self.storeBookUseCase = storeBookUseCase + self.storeMediaUseCase = storeMediaUseCase self.createMediaUseCase = createMediaUseCase self.fetchMediaUseCase = fetchMediaUseCase self.deleteMediaUseCase = deleteMediaUseCase @@ -28,7 +28,7 @@ public struct EditBookViewModelFactory { EditBookViewModel( fetchBookUseCase: fetchBookUseCase, updateBookUseCase: updateBookUseCase, - storeBookUseCase: storeBookUseCase, + storeMediaUseCase: storeMediaUseCase, createMediaUseCase: createMediaUseCase, fetchMediaUseCase: fetchMediaUseCase, deleteMediaUseCase: deleteMediaUseCase From 6f334c555e9c173ac3003e01ba0e4461d903c4f5 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 11:46:21 +0900 Subject: [PATCH 27/60] =?UTF-8?q?refactor:=20=ED=98=84=EC=9E=AC=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95,=20=EC=97=90=EB=9F=AC=20output=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/EditBookViewModel.swift | 110 ++++++++++++------ 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift index fc210313..8d80ba87 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift @@ -7,12 +7,15 @@ final class EditBookViewModel: ViewModelType { // MARK: - Type enum Input { case viewDidLoad(bookID: UUID) - case didAddMediaWithData(type: MediaType, atPage: Int, data: Data) - case didAddMediaInURL(type: MediaType, atPage: Int, url: URL) + case didSelectPage(at: Int) + case didAddMediaWithData(type: MediaType, data: Data) + case didAddMediaInURL(type: MediaType, url: URL) + case addPageButtonTapped case didSaveButtonTapped } enum Output { - case updateTableView + case updateViewController(title: String) + case error(message: String) } // MARK: - Property @@ -20,25 +23,27 @@ final class EditBookViewModel: ViewModelType { private var cancellables = Set() private let fetchBookUseCase: FetchBookUseCase private let updateBookUseCase: UpdateBookUseCase - private let storeBookUseCase: PersistentlyStoreMediaUseCase + private let storeMediaUseCase: PersistentlyStoreMediaUseCase private let createMediaUseCase: CreateMediaUseCase private let fetchMediaUseCase: FetchMediaUseCase private let deleteMediaUseCase: DeleteMediaUseCase private var bookID: UUID? - private var pageViewModels: [EditPageViewModel] = [] + private var title: String = "" + private var editPageViewModels: [EditPageViewModel] = [] + private var currentPageIndex = 0 // MARK: - Initializer init( fetchBookUseCase: FetchBookUseCase, updateBookUseCase: UpdateBookUseCase, - storeBookUseCase: PersistentlyStoreMediaUseCase, + storeMediaUseCase: PersistentlyStoreMediaUseCase, createMediaUseCase: CreateMediaUseCase, fetchMediaUseCase: FetchMediaUseCase, deleteMediaUseCase: DeleteMediaUseCase ) { self.fetchBookUseCase = fetchBookUseCase self.updateBookUseCase = updateBookUseCase - self.storeBookUseCase = storeBookUseCase + self.storeMediaUseCase = storeMediaUseCase self.createMediaUseCase = createMediaUseCase self.fetchMediaUseCase = fetchMediaUseCase self.deleteMediaUseCase = deleteMediaUseCase @@ -50,10 +55,14 @@ final class EditBookViewModel: ViewModelType { switch event { case let .viewDidLoad(bookID): Task { await self?.fetchBook(bookID: bookID) } - case let .didAddMediaWithData(type, atPage, data): - self?.addMedia(type: type, at: atPage, with: data) - case let .didAddMediaInURL(type, atPage, url): - self?.addMedia(type: type, at: atPage, in: url) + case let .didSelectPage(at: index): + self?.currentPageIndex = index + case let .didAddMediaWithData(type, data): + Task { await self?.addMedia(type: type, with: data) } + case let .didAddMediaInURL(type, url): + Task { await self?.addMedia(type: type, in: url) } + case .addPageButtonTapped: + self?.addEmptyPage() case .didSaveButtonTapped: Task { await self?.saveMediaAll() } } @@ -63,47 +72,82 @@ final class EditBookViewModel: ViewModelType { } private func fetchBook(bookID: UUID) async { self.bookID = bookID - guard let book = try? await fetchBookUseCase.execute(bookID: bookID) else { return } - book.pages.forEach { page in - let pageViewModel = EditPageViewModel( - createMediaUseCase: createMediaUseCase, - fetchMediaUseCase: fetchMediaUseCase, - deleteMediaUseCase: deleteMediaUseCase, - bookID: bookID, - page: page - ) - pageViewModels.append(pageViewModel) + do { + let book = try await fetchBookUseCase.execute(id: bookID) + title = book.title + editPageViewModels = book.pages.map { page in + EditPageViewModel( + fetchMediaUseCase: fetchMediaUseCase, + deleteMediaUseCase: deleteMediaUseCase, + bookID: bookID, + page: page + ) + } + output.send(.updateViewController(title: title)) + } catch { + output.send(.error(message: "책을 가져오는데 실패했습니다.")) + MHLogger.error(error.localizedDescription + #function) } - output.send(.updateTableView) } - private func addMedia(type: MediaType, at index: Int, with data: Data) { + private func addMedia(type: MediaType, with data: Data) async { let description = MediaDescription( id: UUID(), type: type ) - pageViewModels[index].addMedia(media: description, data: data) + do { + try await createMediaUseCase.execute(media: description, data: data, at: bookID) + editPageViewModels[currentPageIndex].addMedia(media: description, data: data) + } catch { + output.send(.error(message: "미디어를 추가하는데 실패했습니다.")) + MHLogger.error(error.localizedDescription + #function) + } } - private func addMedia(type: MediaType, at index: Int, in url: URL) { + private func addMedia(type: MediaType, in url: URL) async { let description = MediaDescription( id: UUID(), type: type ) - pageViewModels[index].addMedia(media: description, url: url) + do { + try await createMediaUseCase.execute(media: description, from: url, at: bookID) + editPageViewModels[currentPageIndex].addMedia(media: description, url: url) + } catch { + output.send(.error(message: "미디어를 추가하는데 실패했습니다.")) + MHLogger.error(error.localizedDescription + #function) + } + } + private func addEmptyPage() { + guard let bookID else { return } + let page = Page(id: UUID(), metadata: [:], text: "") + let editPageViewModel = EditPageViewModel( + fetchMediaUseCase: fetchMediaUseCase, + deleteMediaUseCase: deleteMediaUseCase, + bookID: bookID, + page: page + ) + editPageViewModels.append(editPageViewModel) + output.send(.updateViewController(title: title)) } private func saveMediaAll() async { guard let bookID else { return } - pageViewModels.map { viewModel in - + let pages = editPageViewModels.map { $0.page } + let book = Book(id: bookID, title: title, pages: pages) + let mediaList = pages.flatMap { $0.metadata.values } + do { + try await updateBookUseCase.execute(id: bookID, book: book) + try await storeMediaUseCase.execute(to: bookID, mediaList: mediaList) + } catch { + output.send(.error(message: "책을 저장하는데 실패했습니다.")) + MHLogger.error(error.localizedDescription + #function) } - try? await updateBookUseCase.execute(book: <#T##Book#>) - try? await storeBookUseCase.execute(to: bookID) } // MARK: - Method func numberOfPages() -> Int { - return pageViewModels.count + return editPageViewModels.count } - func page(at index: Int) -> EditPageViewModel { - return pageViewModels[index] + func editPageViewModel(at index: Int) -> EditPageViewModel { + let editPageViewModel = editPageViewModels[index] + + return editPageViewModel } } From e98544d40cc6b2cc6e27b593cd158caef7a47aa7 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 11:46:51 +0900 Subject: [PATCH 28/60] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/EditBookViewController.swift | 76 ++++++++----------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index d71543ec..d18f6b86 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -54,10 +54,16 @@ final class EditBookViewController: UIViewController { return stackView }() - private let publishButton: UIButton = { + private let addPageButton: UIButton = { let button = UIButton() - button.setImage(.publishButton, for: .normal) - button.imageView?.contentMode = .scaleAspectFit + let title = NSAttributedString( + string: "페이지 추가", + attributes: [ + .font: UIFont.ownglyphBerry(size: 20), + .foregroundColor: UIColor.mhTitle + ] + ) + button.setAttributedTitle(title, for: .normal) button.backgroundColor = .clear return button @@ -88,13 +94,12 @@ final class EditBookViewController: UIViewController { setup() configureNavigationBar() - configureSaveButton() configureAddSubView() configureConstraints() configureKeyboard() configureBinding() configureButtonAction() - guard let bookID = id else { return } + guard let bookID = id else { return } // TODO: - 나중에 지워야함 input.send(.viewDidLoad(bookID: bookID)) } @@ -136,28 +141,12 @@ final class EditBookViewController: UIViewController { // 네비게이션 오른쪽 아이템 navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "기록", + title: "기록 마치기", normal: normalAttributes, selected: selectedAttributes ) { [weak self] in self?.input.send(.didSaveButtonTapped) - self?.navigationController?.popViewController(animated: true) - } - - // 네비게이션 타이틀 - // TODO: - ViewModel에서 받아오는 타이틀로 변경 - navigationItem.title = "책 제목" - } - private func configureSaveButton() { - // BookCreationViewController에서 넘어온 경우에만 저장 버튼 보여주기 - let isFromCreation = navigationController?.viewControllers - .contains { $0 is BookCreationViewController } ?? false - - if isFromCreation { - navigationItem.rightBarButtonItem = nil - publishButton.isHidden = false - } else { - publishButton.isHidden = true + self?.navigationController?.popToRootViewController(animated: true) } } private func configureAddSubView() { @@ -172,7 +161,7 @@ final class EditBookViewController: UIViewController { view.addSubview(buttonStackView) // publishButton - view.addSubview(publishButton) + view.addSubview(addPageButton) } private func configureConstraints() { // tableView @@ -196,11 +185,9 @@ final class EditBookViewController: UIViewController { buttonStackViewBottomConstraint?.isActive = true // publishButton - publishButton.setAnchor( + addPageButton.setAnchor( bottom: buttonStackView.bottomAnchor, - trailing: editPageTableView.trailingAnchor, constantTrailing: 15, - width: 55, - height: 40 + trailing: editPageTableView.trailingAnchor, constantTrailing: 15 ) } private func configureKeyboard() { @@ -218,25 +205,24 @@ final class EditBookViewController: UIViewController { ) } private func configureBinding() { - // TODO: - 추후 로직 추가하기 let output = viewModel.transform(input: input.eraseToAnyPublisher()) output.receive(on: DispatchQueue.main) .sink { [weak self] event in switch event { - case .updateTableView: + case .updateViewController(title: let title): + self?.navigationItem.title = title self?.editPageTableView.reloadData() + case .error(message: let message): + MHLogger.error(message) // TODO: - Alert 띄우기 } } .store(in: &cancellables) } private func configureButtonAction() { - // TODO: - 로직을 정한다음에 Action 추가 let addImageAction = UIAction { [weak self] _ in // TODO: - 이미지 받는 임시 로직 - guard let data = UIImage(resource: .bookMake).pngData(), - let currentPage = self?.editPageTableView.indexPathForSelectedRow?.row - else { return } - self?.input.send(.didAddMediaWithData(type: .image, atPage: currentPage, data: data)) + guard let data = UIImage(resource: .bookMake).pngData() else { return } + self?.input.send(.didAddMediaWithData(type: .image, data: data)) } addImageButton.addAction(addImageAction, for: .touchUpInside) @@ -250,11 +236,10 @@ final class EditBookViewController: UIViewController { } addAudioButton.addAction(addAudioAction, for: .touchUpInside) - let publishAction = UIAction { [weak self] _ in - self?.input.send(.didSaveButtonTapped) - self?.navigationController?.popViewController(animated: true) + let addPageAction = UIAction { [weak self] _ in + self?.input.send(.addPageButtonTapped) } - publishButton.addAction(publishAction, for: .touchUpInside) + addPageButton.addAction(addPageAction, for: .touchUpInside) } // MARK: - Keyboard Appear & Hide @@ -284,7 +269,9 @@ extension EditBookViewController { // MARK: - UITableViewDelegate extension EditBookViewController: UITableViewDelegate { - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + input.send(.didSelectPage(at: indexPath.row)) + } } // MARK: - UITableViewDataSource @@ -295,14 +282,15 @@ extension EditBookViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell( withIdentifier: EditPageCell.identifier, - for: indexPath - ) as? EditPageCell else { return UITableViewCell() } + for: indexPath) as? EditPageCell + else { return UITableViewCell() } - cell.configure(viewModel: viewModel.page(at: indexPath.row)) + let editPageViewModel = viewModel.editPageViewModel(at: indexPath.row) + cell.configure(viewModel: editPageViewModel) return cell } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return view.frame.height + return view.safeAreaLayoutGuide.layoutFrame.height - buttonStackView.frame.height - 40 } } From acfa79f19c86701c1d0291c9722b52d99841a3fa Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 11:47:09 +0900 Subject: [PATCH 29/60] =?UTF-8?q?feat:=20=EB=AF=B8=EB=94=94=EC=96=B4?= =?UTF-8?q?=EA=B0=80=20=EB=86=92=EC=9D=B4=20=EC=B4=88=EA=B3=BC=EC=8B=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=86=A0=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index c7f2dd8b..c21b33c4 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -51,7 +51,7 @@ final class EditPageCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - saveContents() + viewModel = nil cancellables.removeAll() } @@ -61,6 +61,8 @@ final class EditPageCell: UITableViewCell { selectionStyle = .none textStorage = textView.textStorage + textStorage?.delegate = self + textView.delegate = self } private func configureAddSubView() { contentView.addSubview(textView) @@ -89,6 +91,8 @@ final class EditPageCell: UITableViewCell { self?.mediaLoadedWithData(media: media, data: data) case let .mediaLoadedWithURL(media, url): self?.mediaLoadedWithURL(media: media, url: url) + case let .error(message): + MHLogger.error(message) // 더 좋은 처리가 필요함 } }.store(in: &cancellables) } @@ -110,7 +114,7 @@ final class EditPageCell: UITableViewCell { } private func mediaAddedWithData(media: MediaDescription, data: Data) { let attachment = MediaAttachment( - view: MHPolaroidPhotoView(), + view: MHPolaroidPhotoView(), // TODO: - 수정 필요 description: media ) attachment.configure(with: data) @@ -121,7 +125,7 @@ final class EditPageCell: UITableViewCell { } private func mediaAddedWithURL(media: MediaDescription, url: URL) { let attachment = MediaAttachment( - view: MHPolaroidPhotoView(), + view: MHPolaroidPhotoView(),// TODO: - 수정 필요 description: media ) attachment.configure(with: url) @@ -140,9 +144,8 @@ final class EditPageCell: UITableViewCell { } private func saveContents() { guard let textStorage else { return } - let range = NSRange(location: 0, length: textStorage.length) - let text = textStorage.attributedSubstring(from: range) - input.send(.pageWillDisappear(attributedText: text)) + + input.send(.didEditPage(attributedText: textStorage)) } /// Text에서 특정 Attachment를 찾아서 적용합니다. private func findAttachment( @@ -186,13 +189,17 @@ final class EditPageCell: UITableViewCell { return mutableAttributedString } - private func isAcceptableHight( - _ textView: UITextView, + /// TextView의 높이가 적절한지 확인합니다. + private func isAcceptableHeight( + _ textStorage: NSTextStorage, shouldChangeTextIn range: NSRange, replacementText attributedText: NSAttributedString ) -> Bool { - let updatedText = NSMutableAttributedString(attributedString: textView.attributedText) - let textViewWidth = textView.bounds.width + let updatedText = NSMutableAttributedString(attributedString: textStorage) + let horizontalInset = textView.textContainerInset.left + textView.textContainerInset.right + let verticalInset = textView.textContainerInset.top + textView.textContainerInset.bottom + let textViewWidth = textView.bounds.width - horizontalInset + let textViewHight = textView.bounds.height - verticalInset let temporaryTextView = UITextView( frame: CGRect(x: 0, y: 0, width: textViewWidth, height: .greatestFiniteMagnitude) ) @@ -200,8 +207,7 @@ final class EditPageCell: UITableViewCell { temporaryTextView.attributedText = updatedText temporaryTextView.sizeToFit() - return temporaryTextView.contentSize.height <= textView.bounds.height - || attributedText.string.isEmpty + return temporaryTextView.contentSize.height <= textViewHight } } @@ -215,10 +221,29 @@ extension EditPageCell: @preconcurrency MediaAttachmentDataSource { // MARK: - UITextViewDelegate extension EditPageCell: UITextViewDelegate { func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard let textStorage else { return false } let attributedText = NSAttributedString( string: text, attributes: defaultAttributes ) - return isAcceptableHight(textView, shouldChangeTextIn: range, replacementText: attributedText) + return text.isEmpty + || isAcceptableHeight(textStorage, shouldChangeTextIn: range, replacementText: attributedText) + } +} + +// MARK: - NSTextStorageDelegate +extension EditPageCell: @preconcurrency NSTextStorageDelegate { + func textStorage( + _ textStorage: NSTextStorage, + didProcessEditing editedMask: NSTextStorage.EditActions, + range editedRange: NSRange, + changeInLength delta: Int + ) { + let text = textStorage.attributedSubstring(from: editedRange) + if !isAcceptableHeight(textStorage, shouldChangeTextIn: editedRange, replacementText: text) { + // TODO: - 좀더 우아하게 처리하기? 미리 알려주는 로직으로... 개선필요 + textStorage.deleteCharacters(in: editedRange) + } + saveContents() } } From e4f2f0e61be96dac8b7aa2ea32290351d8ae27d5 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 11:48:14 +0900 Subject: [PATCH 30/60] =?UTF-8?q?feat:=20=EB=A1=9C=EB=94=A9=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=8B=9C=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/EditPageViewModel.swift | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift index 74682f9a..8eaa98c6 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift @@ -7,7 +7,7 @@ final class EditPageViewModel: ViewModelType { // MARK: - Type enum Input { case pageWillAppear - case pageWillDisappear(attributedText: NSAttributedString) + case didEditPage(attributedText: NSAttributedString) case didRequestMediaDataForData(media: MediaDescription) case didRequestMediaDataForURL(media: MediaDescription) } @@ -17,26 +17,24 @@ final class EditPageViewModel: ViewModelType { case mediaAddedWithURL(media: MediaDescription, url: URL) case mediaLoadedWithData(media: MediaDescription, data: Data) case mediaLoadedWithURL(media: MediaDescription, url: URL) + case error(message: String) } // MARK: - Property private let output = PassthroughSubject() private var cancellables = Set() - private let createMediaUseCase: CreateMediaUseCase private let fetchMediaUseCase: FetchMediaUseCase private let deleteMediaUseCase: DeleteMediaUseCase - private var bookID: UUID - private var page: Page + private let bookID: UUID + private(set) var page: Page // MARK: - Initializer init( - createMediaUseCase: CreateMediaUseCase, fetchMediaUseCase: FetchMediaUseCase, deleteMediaUseCase: DeleteMediaUseCase, bookID: UUID, page: Page ) { - self.createMediaUseCase = createMediaUseCase self.fetchMediaUseCase = fetchMediaUseCase self.deleteMediaUseCase = deleteMediaUseCase self.bookID = bookID @@ -49,8 +47,8 @@ final class EditPageViewModel: ViewModelType { switch event { case .pageWillAppear: self?.pageWillAppear() - case .pageWillDisappear(let attributedText): - self?.pageWillDisappear(text: attributedText) + case .didEditPage(let attributedText): + self?.didEditPage(text: attributedText) case .didRequestMediaDataForData(let media): Task { await self?.loadMediaForData(media: media) } case .didRequestMediaDataForURL(let media): @@ -63,19 +61,27 @@ final class EditPageViewModel: ViewModelType { private func pageWillAppear() { output.send(.page(page: page)) } - private func pageWillDisappear(text: NSAttributedString) { + private func didEditPage(text: NSAttributedString) { let page = converTextToPage(text: text) self.page = page } private func loadMediaForData(media: MediaDescription) async { - // TODO: - Loading실패시 로딩실패 처리 - guard let mediaData: Data = try? await fetchMediaUseCase.execute(media: media, in: bookID) else { return } - output.send(.mediaLoadedWithData(media: media, data: mediaData)) + do { + let mediaData: Data = try await fetchMediaUseCase.execute(media: media, in: bookID) + output.send(.mediaLoadedWithData(media: media, data: mediaData)) + } catch { + output.send(.error(message: "미디어 로딩에 실패하였습니다.")) + MHLogger.error(error.localizedDescription + #function) + } } private func loadMediaForURL(media: MediaDescription) async { - // TODO: - Loading실패시 로딩실패 처리 - guard let mediaURL: URL = try? await fetchMediaUseCase.execute(media: media, in: bookID) else { return } - output.send(.mediaLoadedWithURL(media: media, url: mediaURL)) + do { + let mediaURL: URL = try await fetchMediaUseCase.execute(media: media, in: bookID) + output.send(.mediaLoadedWithURL(media: media, url: mediaURL)) + } catch { + output.send(.error(message: "미디어 로딩에 실패하였습니다.")) + MHLogger.error(error.localizedDescription + #function) + } } // MARK: - Method From 2aad98fa1989ca5aa7aa3b7e05e46397d97673c9 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 12:03:08 +0900 Subject: [PATCH 31/60] =?UTF-8?q?refactor:=20BookID=EB=A5=BC=20ViewModel?= =?UTF-8?q?=EC=95=88=EC=9C=BC=EB=A1=9C=20=EB=84=A3=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditBook/View/EditBookViewController.swift | 6 ++---- .../EditBook/ViewModel/EditBookViewModel.swift | 17 ++++++++--------- .../ViewModel/EditBookViewModelFactory.swift | 6 ++++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index d18f6b86..c313098e 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -74,7 +74,6 @@ final class EditBookViewController: UIViewController { private let viewModel: EditBookViewModel private let input = PassthroughSubject() private var cancellables = Set() - var id: UUID? // TODO: - 지워야함 // MARK: - Initializer init(viewModel: EditBookViewModel) { @@ -84,7 +83,7 @@ final class EditBookViewController: UIViewController { } required init?(coder: NSCoder) { guard let viewModel = try? DIContainer.shared.resolve(EditBookViewModelFactory.self) else { return nil } - self.viewModel = viewModel.make() + self.viewModel = viewModel.make(bookID: .init()) super.init(coder: coder) } @@ -99,8 +98,7 @@ final class EditBookViewController: UIViewController { configureKeyboard() configureBinding() configureButtonAction() - guard let bookID = id else { return } // TODO: - 나중에 지워야함 - input.send(.viewDidLoad(bookID: bookID)) + input.send(.viewDidLoad) } // MARK: - Setup & Configuration diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift index 8d80ba87..251099eb 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(bookID: UUID) + case viewDidLoad case didSelectPage(at: Int) case didAddMediaWithData(type: MediaType, data: Data) case didAddMediaInURL(type: MediaType, url: URL) @@ -27,7 +27,7 @@ final class EditBookViewModel: ViewModelType { private let createMediaUseCase: CreateMediaUseCase private let fetchMediaUseCase: FetchMediaUseCase private let deleteMediaUseCase: DeleteMediaUseCase - private var bookID: UUID? + private let bookID: UUID private var title: String = "" private var editPageViewModels: [EditPageViewModel] = [] private var currentPageIndex = 0 @@ -39,7 +39,8 @@ final class EditBookViewModel: ViewModelType { storeMediaUseCase: PersistentlyStoreMediaUseCase, createMediaUseCase: CreateMediaUseCase, fetchMediaUseCase: FetchMediaUseCase, - deleteMediaUseCase: DeleteMediaUseCase + deleteMediaUseCase: DeleteMediaUseCase, + bookID: UUID ) { self.fetchBookUseCase = fetchBookUseCase self.updateBookUseCase = updateBookUseCase @@ -47,14 +48,15 @@ final class EditBookViewModel: ViewModelType { self.createMediaUseCase = createMediaUseCase self.fetchMediaUseCase = fetchMediaUseCase self.deleteMediaUseCase = deleteMediaUseCase + self.bookID = bookID } // MARK: - Binding Method func transform(input: AnyPublisher) -> AnyPublisher { input.sink { [weak self] event in switch event { - case let .viewDidLoad(bookID): - Task { await self?.fetchBook(bookID: bookID) } + case .viewDidLoad: + Task { await self?.fetchBook() } case let .didSelectPage(at: index): self?.currentPageIndex = index case let .didAddMediaWithData(type, data): @@ -70,8 +72,7 @@ final class EditBookViewModel: ViewModelType { return output.eraseToAnyPublisher() } - private func fetchBook(bookID: UUID) async { - self.bookID = bookID + private func fetchBook() async { do { let book = try await fetchBookUseCase.execute(id: bookID) title = book.title @@ -116,7 +117,6 @@ final class EditBookViewModel: ViewModelType { } } private func addEmptyPage() { - guard let bookID else { return } let page = Page(id: UUID(), metadata: [:], text: "") let editPageViewModel = EditPageViewModel( fetchMediaUseCase: fetchMediaUseCase, @@ -128,7 +128,6 @@ final class EditBookViewModel: ViewModelType { output.send(.updateViewController(title: title)) } private func saveMediaAll() async { - guard let bookID else { return } let pages = editPageViewModels.map { $0.page } let book = Book(id: bookID, title: title, pages: pages) let mediaList = pages.flatMap { $0.metadata.values } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift index 6d6f7468..69509fb1 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModelFactory.swift @@ -1,3 +1,4 @@ +import MHFoundation import MHDomain public struct EditBookViewModelFactory { @@ -24,14 +25,15 @@ public struct EditBookViewModelFactory { self.deleteMediaUseCase = deleteMediaUseCase } - func make() -> EditBookViewModel { + func make(bookID: UUID) -> EditBookViewModel { EditBookViewModel( fetchBookUseCase: fetchBookUseCase, updateBookUseCase: updateBookUseCase, storeMediaUseCase: storeMediaUseCase, createMediaUseCase: createMediaUseCase, fetchMediaUseCase: fetchMediaUseCase, - deleteMediaUseCase: deleteMediaUseCase + deleteMediaUseCase: deleteMediaUseCase, + bookID: bookID ) } } From d8ae689e3a6c65046965310aa73ea30d832d7366 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 15:30:11 +0900 Subject: [PATCH 32/60] =?UTF-8?q?refactor:=20=ED=82=A4=EB=B3=B4=EB=93=9C?= =?UTF-8?q?=20=EB=82=B4=EB=A6=AC=EB=8A=94=20=EB=A1=9C=EC=A7=81=20tableView?= =?UTF-8?q?=EB=A1=9C=20=EB=84=98=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditBookViewController.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index c313098e..d30472d3 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -201,6 +201,7 @@ final class EditBookViewController: UIViewController { name: UIResponder.keyboardWillHideNotification, object: nil ) + editPageTableView.keyboardDismissMode = .onDrag } private func configureBinding() { let output = viewModel.transform(input: input.eraseToAnyPublisher()) @@ -258,13 +259,6 @@ final class EditBookViewController: UIViewController { } } -// MARK: - UIScrollViewDelegate -extension EditBookViewController { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - view.endEditing(true) - } -} - // MARK: - UITableViewDelegate extension EditBookViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { From 2aa3cdc3e3ba9924474f7f688921088ff8e29505 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 15:31:45 +0900 Subject: [PATCH 33/60] =?UTF-8?q?feat:=20=EC=97=86=EC=96=B4=EC=A7=88=20?= =?UTF-8?q?=EB=95=8C=20=EA=B5=AC=EB=8F=85=20=EC=B7=A8=EC=86=8C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHPresentation/Source/EditBook/View/EditPageCell.swift | 5 ++++- .../Source/EditBook/ViewModel/EditPageViewModel.swift | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index c21b33c4..d6f2b804 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -51,8 +51,11 @@ final class EditPageCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + input.send(.pageWillDisappear) + cancellables.forEach { $0.cancel() } + cancellables = [] viewModel = nil - cancellables.removeAll() + textView.text = "" } // MARK: - Setup & Configuration diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift index 8eaa98c6..683d7c03 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift @@ -7,6 +7,7 @@ final class EditPageViewModel: ViewModelType { // MARK: - Type enum Input { case pageWillAppear + case pageWillDisappear case didEditPage(attributedText: NSAttributedString) case didRequestMediaDataForData(media: MediaDescription) case didRequestMediaDataForURL(media: MediaDescription) @@ -47,6 +48,8 @@ final class EditPageViewModel: ViewModelType { switch event { case .pageWillAppear: self?.pageWillAppear() + case .pageWillDisappear: + self?.pageWillDisappear() case .didEditPage(let attributedText): self?.didEditPage(text: attributedText) case .didRequestMediaDataForData(let media): @@ -61,6 +64,10 @@ final class EditPageViewModel: ViewModelType { private func pageWillAppear() { output.send(.page(page: page)) } + private func pageWillDisappear() { + cancellables.forEach { $0.cancel() } + cancellables = [] + } private func didEditPage(text: NSAttributedString) { let page = converTextToPage(text: text) self.page = page From fefb2e3e989e2b6266af97cc11fbec018e21d07e Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 15:33:20 +0900 Subject: [PATCH 34/60] =?UTF-8?q?feat:=20Delegate=EB=A1=9C=20=ED=98=84?= =?UTF-8?q?=EC=9E=AC=20=EC=84=A0=ED=83=9D=EB=90=9C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20index=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/EditBookViewController.swift | 7 ++----- .../Source/EditBook/View/EditPageCell.swift | 3 +++ .../ViewModel/EditBookViewModel.swift | 19 +++++++++++++------ .../ViewModel/EditPageViewModel.swift | 11 +++++++++++ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index d30472d3..06233552 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -261,8 +261,8 @@ final class EditBookViewController: UIViewController { // MARK: - UITableViewDelegate extension EditBookViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - input.send(.didSelectPage(at: indexPath.row)) + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return view.safeAreaLayoutGuide.layoutFrame.height - buttonStackView.frame.height - 40 } } @@ -282,7 +282,4 @@ extension EditBookViewController: UITableViewDataSource { return cell } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return view.safeAreaLayoutGuide.layoutFrame.height - buttonStackView.frame.height - 40 - } } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index d6f2b804..98f42837 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -232,6 +232,9 @@ extension EditPageCell: UITextViewDelegate { return text.isEmpty || isAcceptableHeight(textStorage, shouldChangeTextIn: range, replacementText: attributedText) } + func textViewDidBeginEditing(_ textView: UITextView) { + input.send(.didBeginEditingPage) + } } // MARK: - NSTextStorageDelegate diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift index 251099eb..7b89b4a9 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift @@ -1,5 +1,5 @@ import MHFoundation -@preconcurrency import Combine +import Combine import MHDomain import MHCore @@ -7,7 +7,6 @@ final class EditBookViewModel: ViewModelType { // MARK: - Type enum Input { case viewDidLoad - case didSelectPage(at: Int) case didAddMediaWithData(type: MediaType, data: Data) case didAddMediaInURL(type: MediaType, url: URL) case addPageButtonTapped @@ -57,8 +56,6 @@ final class EditBookViewModel: ViewModelType { switch event { case .viewDidLoad: Task { await self?.fetchBook() } - case let .didSelectPage(at: index): - self?.currentPageIndex = index case let .didAddMediaWithData(type, data): Task { await self?.addMedia(type: type, with: data) } case let .didAddMediaInURL(type, url): @@ -77,12 +74,14 @@ final class EditBookViewModel: ViewModelType { let book = try await fetchBookUseCase.execute(id: bookID) title = book.title editPageViewModels = book.pages.map { page in - EditPageViewModel( + let editPageViewModel = EditPageViewModel( fetchMediaUseCase: fetchMediaUseCase, deleteMediaUseCase: deleteMediaUseCase, bookID: bookID, page: page ) + editPageViewModel.delegate = self + return editPageViewModel } output.send(.updateViewController(title: title)) } catch { @@ -124,6 +123,7 @@ final class EditBookViewModel: ViewModelType { bookID: bookID, page: page ) + editPageViewModel.delegate = self editPageViewModels.append(editPageViewModel) output.send(.updateViewController(title: title)) } @@ -140,7 +140,7 @@ final class EditBookViewModel: ViewModelType { } } - // MARK: - Method + // MARK: - Method For ViewController func numberOfPages() -> Int { return editPageViewModels.count } @@ -150,3 +150,10 @@ final class EditBookViewModel: ViewModelType { return editPageViewModel } } + +extension EditBookViewModel: EditPageViewModelDelegate { + func didBeginEditingPage(_ editPageViewModel: EditPageViewModel, page: Page) { + let pageID = page.id + currentPageIndex = editPageViewModels.firstIndex { $0.page.id == pageID } ?? 0 + } +} diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift index 683d7c03..7f182011 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift @@ -3,11 +3,16 @@ import MHFoundation import MHDomain import MHCore +protocol EditPageViewModelDelegate: AnyObject { + func didBeginEditingPage(_ editPageViewModel: EditPageViewModel, page: Page) +} + final class EditPageViewModel: ViewModelType { // MARK: - Type enum Input { case pageWillAppear case pageWillDisappear + case didBeginEditingPage case didEditPage(attributedText: NSAttributedString) case didRequestMediaDataForData(media: MediaDescription) case didRequestMediaDataForURL(media: MediaDescription) @@ -26,6 +31,7 @@ final class EditPageViewModel: ViewModelType { private var cancellables = Set() private let fetchMediaUseCase: FetchMediaUseCase private let deleteMediaUseCase: DeleteMediaUseCase + weak var delegate: EditPageViewModelDelegate? private let bookID: UUID private(set) var page: Page @@ -50,6 +56,8 @@ final class EditPageViewModel: ViewModelType { self?.pageWillAppear() case .pageWillDisappear: self?.pageWillDisappear() + case .didBeginEditingPage: + self?.didBeginEditingPage() case .didEditPage(let attributedText): self?.didEditPage(text: attributedText) case .didRequestMediaDataForData(let media): @@ -98,6 +106,9 @@ final class EditPageViewModel: ViewModelType { func addMedia(media: MediaDescription, url: URL) { output.send(.mediaAddedWithURL(media: media, url: url)) } + func didBeginEditingPage() { + delegate?.didBeginEditingPage(self, page: page) + } // MARK: - Helper private func converTextToPage(text: NSAttributedString) -> Page { From 73cf9174a811383139aab2c55b92c8f2ddff1ead Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 15:33:43 +0900 Subject: [PATCH 35/60] =?UTF-8?q?refactor:=20combine=EC=97=90=EC=84=9C=20p?= =?UTF-8?q?reconcurrency=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/ViewModel/EditPageViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift index 7f182011..5e714647 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditPageViewModel.swift @@ -1,5 +1,5 @@ import MHFoundation -@preconcurrency import Combine +import Combine import MHDomain import MHCore From 165df5432abb245a41c2672a39e571e961153c00 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 22:22:16 +0900 Subject: [PATCH 36/60] =?UTF-8?q?chore:=20indention=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 98f42837..287707cd 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -83,21 +83,21 @@ final class EditPageCell: UITableViewCell { output? .receive(on: DispatchQueue.main) .sink { [weak self] event in - switch event { - case .page(let page): - self?.configurePage(page: page) - case let .mediaAddedWithData(media, data): - self?.mediaAddedWithData(media: media, data: data) - case let .mediaAddedWithURL(media, url): - self?.mediaAddedWithURL(media: media, url: url) - case let .mediaLoadedWithData(media, data): - self?.mediaLoadedWithData(media: media, data: data) - case let .mediaLoadedWithURL(media, url): - self?.mediaLoadedWithURL(media: media, url: url) - case let .error(message): - MHLogger.error(message) // 더 좋은 처리가 필요함 - } - }.store(in: &cancellables) + switch event { + case .page(let page): + self?.configurePage(page: page) + case let .mediaAddedWithData(media, data): + self?.mediaAddedWithData(media: media, data: data) + case let .mediaAddedWithURL(media, url): + self?.mediaAddedWithURL(media: media, url: url) + case let .mediaLoadedWithData(media, data): + self?.mediaLoadedWithData(media: media, data: data) + case let .mediaLoadedWithURL(media, url): + self?.mediaLoadedWithURL(media: media, url: url) + case let .error(message): + MHLogger.error(message) // 더 좋은 처리가 필요함 + } + }.store(in: &cancellables) } // MARK: - Method From 09e3f667136183b0327fcdd098760fa6e5488ebc Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 22:23:30 +0900 Subject: [PATCH 37/60] =?UTF-8?q?feat:=20attachment=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=EC=A0=84=EC=97=90=20=EB=93=9C=EB=9E=98=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 287707cd..93108643 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -225,10 +225,19 @@ extension EditPageCell: @preconcurrency MediaAttachmentDataSource { extension EditPageCell: UITextViewDelegate { func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { guard let textStorage else { return false } - let attributedText = NSAttributedString( + let attributedText = NSMutableAttributedString( string: text, attributes: defaultAttributes ) + + // Attachment지우기 전에 드래그해서 알려주기 + if text.isEmpty && range.length == 1 + && attachmentAt(range.location) != nil + && textView.selectedRange.length == 0 { + textView.selectedRange = NSRange(location: range.location, length: 1) + return false + } + return text.isEmpty || isAcceptableHeight(textStorage, shouldChangeTextIn: range, replacementText: attributedText) } @@ -252,4 +261,10 @@ extension EditPageCell: @preconcurrency NSTextStorageDelegate { } saveContents() } + // 그곳에 Attachment가 있는지 확인합니다. + private func attachmentAt(_ index: Int) -> MediaAttachment? { + guard let textStorage else { return nil } + guard index >= 0 && index < textStorage.length else { return nil } + return textStorage.attributes(at: index, effectiveRange: nil)[.attachment] as? MediaAttachment + } } From 961e538bd305e7292837a121bf64235b8fb29f21 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 22:23:49 +0900 Subject: [PATCH 38/60] =?UTF-8?q?chore:=20indention=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 93108643..2dc8c873 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -161,12 +161,12 @@ final class EditPageCell: UITableViewCell { .attachment, in: NSRange(location: 0, length: textStorage.length) ) { value, _, _ in - if let mediaAttachment = value as? MediaAttachment, - mediaAttachment.mediaDescription.id == media.id { - attachment = mediaAttachment - return + if let mediaAttachment = value as? MediaAttachment, + mediaAttachment.mediaDescription.id == media.id { + attachment = mediaAttachment + return + } } - } return attachment } /// Text와 Attachment 정보를 하나의 문자열로 조합합니다. From 2bc516ae6b82a106cb58c4f21d13015258449b28 Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 22:24:28 +0900 Subject: [PATCH 39/60] =?UTF-8?q?refactor:=20reloading=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/MediaAttachment.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift index 42af17d7..10be10ea 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift @@ -9,6 +9,7 @@ protocol MediaAttachmentDataSource: AnyObject { final class MediaAttachment: NSTextAttachment { // MARK: - Property private let view: (UIView & MediaAttachable) + var cachedViewProvider: MediaAttachmentViewProvider? let mediaDescription: MediaDescription weak var dataSource: MediaAttachmentDataSource? @@ -29,6 +30,9 @@ final class MediaAttachment: NSTextAttachment { location: any NSTextLocation, textContainer: NSTextContainer? ) -> NSTextAttachmentViewProvider? { + if let provider = cachedViewProvider { + return provider + } let provider = MediaAttachmentViewProvider( textAttachment: self, parentView: parentView, @@ -38,6 +42,8 @@ final class MediaAttachment: NSTextAttachment { provider.tracksTextAttachmentViewBounds = true provider.view = view provider.type = mediaDescription.type + cachedViewProvider = provider + return provider } override func image( @@ -45,6 +51,7 @@ final class MediaAttachment: NSTextAttachment { textContainer: NSTextContainer?, characterIndex charIndex: Int ) -> UIImage? { + cachedViewProvider = nil return dataSource?.mediaAttachmentDragingImage(self, about: view) } From 43c4a1a1455ee55ae268d7cb273f287b2b89321f Mon Sep 17 00:00:00 2001 From: iceHood Date: Sun, 1 Dec 2024 22:25:31 +0900 Subject: [PATCH 40/60] =?UTF-8?q?feat:=20attachment=20=EC=A0=84/=ED=9B=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9E=85=EB=A0=A5=EC=8B=9C=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EA=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 2dc8c873..afe6a1c9 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -248,6 +248,43 @@ extension EditPageCell: UITextViewDelegate { // MARK: - NSTextStorageDelegate extension EditPageCell: @preconcurrency NSTextStorageDelegate { + func textStorage( + _ textStorage: NSTextStorage, + willProcessEditing editedMask: NSTextStorage.EditActions, + range editedRange: NSRange, + changeInLength delta: Int + ) { + // 입력하는 곳 앞에 Attachment가 있을 때, 줄바꿈을 추가합니다. + if editedRange.location > 0, delta > 0, + attachmentAt(editedRange.location - 1) != nil { + textStorage.insert( + NSAttributedString( + string: "\n", + attributes: defaultAttributes + ), + at: editedRange.location + ) + textView.selectedRange = NSRange(location: editedRange.location + 1, length: 0) + } + + // 입력하는 곳 뒤에 Attachment가 있을 때, 줄바꿈을 추가합니다. + let nextIndex = editedRange.location + editedRange.length + if nextIndex < textStorage.length, + let attachment = attachmentAt(nextIndex) { + attachment.cachedViewProvider = nil + // 입력하려는 문자끝에 \n이 있으면 아래 로직 무시 + guard textStorage.attributedSubstring( + from: NSRange(location: editedRange.location, length: 1) + ).string != "\n" else { return } + textStorage.insert( + NSAttributedString( + string: "\n", + attributes: defaultAttributes + ), + at: nextIndex + ) + } + } func textStorage( _ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorage.EditActions, @@ -255,6 +292,12 @@ extension EditPageCell: @preconcurrency NSTextStorageDelegate { changeInLength delta: Int ) { let text = textStorage.attributedSubstring(from: editedRange) + + let nextIndex = editedRange.location + editedRange.length + if nextIndex < textStorage.length, editedRange.length >= 1, + let attachment = attachmentAt(nextIndex) { + attachment.cachedViewProvider = nil + } if !isAcceptableHeight(textStorage, shouldChangeTextIn: editedRange, replacementText: text) { // TODO: - 좀더 우아하게 처리하기? 미리 알려주는 로직으로... 개선필요 textStorage.deleteCharacters(in: editedRange) From 878ebf31958b8b0b0fd39aa1ec7320a812180447 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 00:00:46 +0900 Subject: [PATCH 41/60] =?UTF-8?q?refactor:=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=95=88=EB=82=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHPresentation/Source/EditBook/View/EditPageCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index afe6a1c9..f193853b 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -255,7 +255,7 @@ extension EditPageCell: @preconcurrency NSTextStorageDelegate { changeInLength delta: Int ) { // 입력하는 곳 앞에 Attachment가 있을 때, 줄바꿈을 추가합니다. - if editedRange.location > 0, delta > 0, + if editedRange.location - 1 > 0, delta > 0, attachmentAt(editedRange.location - 1) != nil { textStorage.insert( NSAttributedString( From 4e6f1e0edda74a9f22de5151d78a778838dfd5d0 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 00:59:25 +0900 Subject: [PATCH 42/60] =?UTF-8?q?feat:=20BookCreation=EC=8B=9C=20editbook?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/BookCreation/BookCreationViewController.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewController.swift index 7f19b61e..ef576673 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewController.swift @@ -1,4 +1,5 @@ import UIKit +import MHCore import Combine final class BookCreationViewController: UIViewController { @@ -210,10 +211,12 @@ final class BookCreationViewController: UIViewController { normal: normalAttributes, selected: selectedAttributes ) { [weak self] in - // TODO: - 추후 뷰모델 관련 생성 이슈 조정 필요 - let editBookViewModel = EditBookViewModel() + // TODO: - 추후 DIContainer resolve 실패 처리 필요 + // TODO: - bookID에 bookCoverID 넣어주기 필요 + guard let editBookViewModelFactory = try? DIContainer.shared.resolve(EditBookViewModelFactory.self) else { return } + let viewModel = editBookViewModelFactory.make(bookID: .init()) self?.navigationController?.pushViewController( - EditBookViewController(viewModel: editBookViewModel), + EditBookViewController(viewModel: viewModel), animated: true ) } From 6c475ec82a45f0815357a47a67dca9731662e272 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:04:56 +0900 Subject: [PATCH 43/60] =?UTF-8?q?refactor:=20guard=20=EB=AC=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHApplication/MHApplication/Source/App/SceneDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift index 84b2bd1e..b48844a8 100644 --- a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift +++ b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift @@ -136,7 +136,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { BookRepository.self, object: LocalBookRepository(storage: bookStorage) ) - guard let fileManager = try? DIContainer.shared.resolve(MHFileManager.self) else { return } + let fileManager = try DIContainer.shared.resolve(MHFileManager.self) DIContainer.shared.register( MediaRepository.self, object: LocalMediaRepository(storage: fileManager) From 47ead3817319b05b36819a47ec7f37831679275b Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:06:09 +0900 Subject: [PATCH 44/60] =?UTF-8?q?refactor:=20fileManager=20register=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHApplication/Source/App/SceneDelegate.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift index b48844a8..eca10b8c 100644 --- a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift +++ b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift @@ -92,10 +92,6 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { CoreDataBookStorage.self, object: CoreDataBookStorage(coreDataStorage: coreDataStorage) ) - DIContainer.shared.register( - MHFileManager.self, - object: MHFileManager(directoryType: .documentDirectory) - ) DIContainer.shared.register( BookCategoryStorage.self, object: CoreDataBookCategoryStorage(coreDataStorage: coreDataStorage) @@ -112,6 +108,10 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { MemorialHouseNameStorage.self, object: UserDefaultsMemorialHouseNameStorage() ) + DIContainer.shared.register( + MHFileManager.self, + object: MHFileManager(directoryType: .documentDirectory) + ) } private func registerRepositoryDependency() throws { From 54358d3b9a9289e5d3eb6301ca753dd9ea54a72a Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:10:30 +0900 Subject: [PATCH 45/60] =?UTF-8?q?chore:=20=EC=97=86=EC=96=B4=EC=A7=88=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=20todo=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHData/MHData/Repository/LocalMediaRepository.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift b/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift index 440fa203..b45639df 100644 --- a/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift +++ b/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift @@ -7,7 +7,7 @@ import AVFoundation // TODO: nil이라면 바로 error를 return하도록 수정 public struct LocalMediaRepository: MediaRepository, Sendable { private let storage: FileStorage - private let temporaryPath = "temp" + private let temporaryPath = "temp" // TODO: - 지워질 것임! private let snapshotFileName = ".snapshot" public init(storage: FileStorage) { From 340a3cfee8a5dfe348bb150962e0352a011d1ac4 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:12:18 +0900 Subject: [PATCH 46/60] =?UTF-8?q?refactor:=20mediaRepository=20read=20->?= =?UTF-8?q?=20fetch=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHData/MHData/Repository/LocalMediaRepository.swift | 2 +- .../MHDomain/MHDomain/Repository/MediaRepository.swift | 2 +- .../MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift b/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift index b45639df..efc45fb4 100644 --- a/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift +++ b/MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift @@ -39,7 +39,7 @@ public struct LocalMediaRepository: MediaRepository, Sendable { return await storage.copy(at: from, to: path, newFileName: fileName) } - public func read( + public func fetch( media mediaDescription: MediaDescription, from bookID: UUID? ) async -> Result { diff --git a/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift b/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift index 25809869..59fac929 100644 --- a/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift +++ b/MemorialHouse/MHDomain/MHDomain/Repository/MediaRepository.swift @@ -5,7 +5,7 @@ import Photos public protocol MediaRepository: Sendable { func create(media mediaDescription: MediaDescription, data: Data, to bookID: UUID?) async -> Result func create(media mediaDescription: MediaDescription, from: URL, to bookID: UUID?) async -> Result - func read(media mediaDescription: MediaDescription, from bookID: UUID?) async -> Result + func fetch(media mediaDescription: MediaDescription, from bookID: UUID?) async -> Result func getURL(media mediaDescription: MediaDescription, from bookID: UUID?) async -> Result func delete(media mediaDescription: MediaDescription, at bookID: UUID?) async -> Result func moveTemporaryMedia(_ mediaDescription: MediaDescription, to bookID: UUID) async -> Result diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift index af69fe70..3651a250 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift @@ -31,9 +31,9 @@ public struct DefaultFetchMediaUseCase: FetchMediaUseCase { // MARK: - Method public func execute(media: MediaDescription, in bookID: UUID) async throws -> Data { do { - return try await repository.read(media: media, from: nil).get() + return try await repository.fetch(media: media, from: nil).get() } catch { - return try await repository.read(media: media, from: bookID).get() + return try await repository.fetch(media: media, from: bookID).get() } } public func execute(media: MediaDescription, in bookID: UUID) async throws -> URL { From 9a136aa7e02d065d3373045f87fe6eb4b977d8d5 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:14:27 +0900 Subject: [PATCH 47/60] =?UTF-8?q?chore:=20=EC=97=86=EC=96=B4=EC=A7=88=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=20todo=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift | 8 ++++---- .../MHDomain/UseCase/Interface/MediaUseCase.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift index 3651a250..a3076ec6 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift @@ -31,14 +31,14 @@ public struct DefaultFetchMediaUseCase: FetchMediaUseCase { // MARK: - Method public func execute(media: MediaDescription, in bookID: UUID) async throws -> Data { do { - return try await repository.fetch(media: media, from: nil).get() + return try await repository.fetch(media: media, from: nil).get() // TODO: - 없어질 로직 } catch { return try await repository.fetch(media: media, from: bookID).get() } } public func execute(media: MediaDescription, in bookID: UUID) async throws -> URL { do { - return try await repository.getURL(media: media, from: nil).get() + return try await repository.getURL(media: media, from: nil).get() // TODO: - 없어질 로직 } catch { return try await repository.getURL(media: media, from: bookID).get() } @@ -57,7 +57,7 @@ public struct DefaultDeleteMediaUseCase: DeleteMediaUseCase { // MARK: - Method public func execute(media: MediaDescription, in bookID: UUID) async throws { do { - return try await repository.delete(media: media, at: nil).get() + return try await repository.delete(media: media, at: nil).get() // TODO: - 없어질 로직 } catch { return try await repository.delete(media: media, at: bookID).get() } @@ -74,7 +74,7 @@ public struct DefaultPersistentlyStoreMediaUseCase: PersistentlyStoreMediaUseCas } // MARK: - Method - public func execute(to bookID: UUID) async throws { + public func execute(to bookID: UUID) async throws { // TODO: - 없어질 로직 try await repository.moveAllTemporaryMedia(to: bookID).get() } public func execute(to bookID: UUID, mediaList: [MediaDescription]) async throws { diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift index 68a73098..fc43406c 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift @@ -16,6 +16,6 @@ public protocol DeleteMediaUseCase: Sendable { public protocol PersistentlyStoreMediaUseCase: Sendable { @available(*, deprecated, message: "temp를 더이상 사용하지 않습니다.") - func execute(to bookID: UUID) async throws + func execute(to bookID: UUID) async throws // TODO: - 없애야함 func execute(to bookID: UUID, mediaList: [MediaDescription]) async throws } From d60becb6d038c51a42d544f28eb37610ccc327cf Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:17:40 +0900 Subject: [PATCH 48/60] =?UTF-8?q?refactor:=20BookCreationVC=20->=20CreatBo?= =?UTF-8?q?okVC=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Controller.swift => CreateBookViewController.swift} | 10 +++++----- ...eationViewModel.swift => CreateBookViewModel.swift} | 2 +- .../Source/Home/HomeViewController.swift | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/{BookCreationViewController.swift => CreateBookViewController.swift} (98%) rename MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/{BookCreationViewModel.swift => CreateBookViewModel.swift} (94%) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift similarity index 98% rename from MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewController.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift index ef576673..32a3bae0 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift @@ -2,7 +2,7 @@ import UIKit import MHCore import Combine -final class BookCreationViewController: UIViewController { +final class CreateBookViewController: UIViewController { // MARK: - Constant static let maxTitleLength = 10 // MARK: - Property @@ -76,17 +76,17 @@ final class BookCreationViewController: UIViewController { return shadowLayer }() @Published - private var viewModel: BookCreationViewModel + private var viewModel: CreateBookViewModel private var cancellables: Set = [] // MARK: - Initializer - init(viewModel: BookCreationViewModel) { + init(viewModel: CreateBookViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { - viewModel = BookCreationViewModel() + viewModel = CreateBookViewModel() super.init(coder: coder) } @@ -333,7 +333,7 @@ final class BookCreationViewController: UIViewController { } } -extension BookCreationViewController: UITextFieldDelegate { +extension CreateBookViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewModel.swift similarity index 94% rename from MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewModel.swift rename to MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewModel.swift index b850ab36..432868f8 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/BookCreationViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewModel.swift @@ -2,7 +2,7 @@ import MHDomain import MHFoundation import Photos -struct BookCreationViewModel { +struct CreateBookViewModel { var bookTitle: String = "" var bookCategory: String = "" var previousColorNumber: Int = -1 diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift index 2a385e96..8d85bcb9 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift @@ -159,8 +159,8 @@ public final class HomeViewController: UIViewController { makingBookFloatingButton.addAction(UIAction { [weak self] _ in guard let self else { return } - let bookCreationViewController = BookCreationViewController(viewModel: BookCreationViewModel()) - self.navigationController?.pushViewController(bookCreationViewController, animated: true) + let createBookViewController = CreateBookViewController(viewModel: CreateBookViewModel()) + self.navigationController?.pushViewController(createBookViewController, animated: true) }, for: .touchUpInside) navigationBar.configureSettingAction(action: UIAction { [weak self] _ in From 68a2b0d29117c0d97db3f8fc227c6a616dffc158 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:18:38 +0900 Subject: [PATCH 49/60] =?UTF-8?q?refactor:=20Combine=EC=9D=98=20@preconcur?= =?UTF-8?q?rency=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditBookViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index 06233552..f61c0f95 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -1,6 +1,6 @@ import UIKit import MHCore -@preconcurrency import Combine +import Combine final class EditBookViewController: UIViewController { // MARK: - Constant From ad14f849c9eed5e52c1cd37bda8d5600b9ec37ef Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:19:47 +0900 Subject: [PATCH 50/60] =?UTF-8?q?chore:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditBookViewController.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index f61c0f95..937218d4 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -148,21 +148,17 @@ final class EditBookViewController: UIViewController { } } private func configureAddSubView() { - // editPageTableView view.addSubview(editPageTableView) - // buttonStackView buttonStackView.addArrangedSubview(addImageButton) buttonStackView.addArrangedSubview(addTextButton) buttonStackView.addArrangedSubview(addVideoButton) buttonStackView.addArrangedSubview(addAudioButton) view.addSubview(buttonStackView) - // publishButton view.addSubview(addPageButton) } private func configureConstraints() { - // tableView editPageTableView.setAnchor( top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, @@ -170,7 +166,6 @@ final class EditBookViewController: UIViewController { trailing: view.trailingAnchor ) - // buttonStackView buttonStackView.setAnchor( leading: editPageTableView.leadingAnchor, constantLeading: 10, height: 40 @@ -182,7 +177,6 @@ final class EditBookViewController: UIViewController { ) buttonStackViewBottomConstraint?.isActive = true - // publishButton addPageButton.setAnchor( bottom: buttonStackView.bottomAnchor, trailing: editPageTableView.trailingAnchor, constantTrailing: 15 From d019cfe8a93fe4bbb126a22633a38c0ce1bbd21e Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:23:32 +0900 Subject: [PATCH 51/60] =?UTF-8?q?chore:=20=EC=84=A4=EB=AA=85=EC=9A=A9=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditBookViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index 937218d4..01693ea4 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -195,6 +195,7 @@ final class EditBookViewController: UIViewController { name: UIResponder.keyboardWillHideNotification, object: nil ) + // 스크롤이 될 때 키보드 내려가게 설정 editPageTableView.keyboardDismissMode = .onDrag } private func configureBinding() { From bb0924c9136a397bf31c1d377fb99080e5bb8ed3 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:26:46 +0900 Subject: [PATCH 52/60] =?UTF-8?q?chore:=20case=20let=20=EA=B5=AC=EB=AC=B8?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditBookViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index 01693ea4..fafd0e0f 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -203,7 +203,7 @@ final class EditBookViewController: UIViewController { output.receive(on: DispatchQueue.main) .sink { [weak self] event in switch event { - case .updateViewController(title: let title): + case let .updateViewController(title): self?.navigationItem.title = title self?.editPageTableView.reloadData() case .error(message: let message): From 3f66fd06babe1b658045c36122c92e3c0decc42a Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 09:31:24 +0900 Subject: [PATCH 53/60] =?UTF-8?q?refactor:=20if=EB=AC=B8=20guard=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index f193853b..ace67949 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -161,11 +161,9 @@ final class EditPageCell: UITableViewCell { .attachment, in: NSRange(location: 0, length: textStorage.length) ) { value, _, _ in - if let mediaAttachment = value as? MediaAttachment, - mediaAttachment.mediaDescription.id == media.id { - attachment = mediaAttachment - return - } + guard let mediaAttachment = value as? MediaAttachment, + mediaAttachment.mediaDescription.id == media.id else { return } + attachment = mediaAttachment } return attachment } From 4159dc35434c3aee3d3e2bc46fab03d187fa713b Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 10:01:28 +0900 Subject: [PATCH 54/60] =?UTF-8?q?refactor:=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=EC=9D=80=20=ED=95=98=EA=B2=8C=EB=81=94=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookCreation/CreateBookViewController.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift index 32a3bae0..da0fd209 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift @@ -1,4 +1,5 @@ import UIKit +import MHDomain // TODO: - 추후 로직에 따라 제거 필요 import MHCore import Combine @@ -213,12 +214,16 @@ final class CreateBookViewController: UIViewController { ) { [weak self] in // TODO: - 추후 DIContainer resolve 실패 처리 필요 // TODO: - bookID에 bookCoverID 넣어주기 필요 - guard let editBookViewModelFactory = try? DIContainer.shared.resolve(EditBookViewModelFactory.self) else { return } - let viewModel = editBookViewModelFactory.make(bookID: .init()) - self?.navigationController?.pushViewController( - EditBookViewController(viewModel: viewModel), - animated: true - ) + Task { + guard let editBookViewModelFactory = try? DIContainer.shared.resolve(EditBookViewModelFactory.self) else { return } + let book = Book(id: .init(), title: "HIHI", pages: [.init(metadata: [:], text: "")]) + try? await DIContainer.shared.resolve(BookRepository.self).create(book: book) + let viewModel = editBookViewModelFactory.make(bookID: book.id) + self?.navigationController?.pushViewController( + EditBookViewController(viewModel: viewModel), + animated: true + ) + } } } From 7a2a84a371705e747ce8b7f2723e9826703957ae Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 10:02:31 +0900 Subject: [PATCH 55/60] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=A7=81=EA=B4=80=EC=A0=81=EC=9D=B4?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/EditBook/View/EditPageCell.swift | 93 +++++++++---------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index ace67949..58026092 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -115,6 +115,29 @@ final class EditPageCell: UITableViewCell { ) textStorage?.setAttributedString(mergedText) } + /// Text와 Attachment 정보를 하나의 문자열로 조합합니다. + private func mergeStorageInformation( + text: String, + attachmentMetaData: [Int: MediaDescription] + ) -> NSAttributedString { + let mutableAttributedString = NSMutableAttributedString(string: text) + attachmentMetaData.forEach { location, description in + let range = NSRange(location: location, length: 1) + let mediaAttachment = MediaAttachment( + view: MHPolaroidPhotoView(), // TODO: - 이거 바꿔줘야함... + description: description + ) + input.send(.didRequestMediaDataForData(media: description)) + let attachmentString = NSAttributedString(attachment: mediaAttachment) + // Placeholder(공백) 교체 + mutableAttributedString.replaceCharacters(in: range, with: attachmentString) + } + + mutableAttributedString.addAttributes(defaultAttributes, + range: NSRange(location: 0, length: mutableAttributedString.length)) + + return mutableAttributedString + } private func mediaAddedWithData(media: MediaDescription, data: Data) { let attachment = MediaAttachment( view: MHPolaroidPhotoView(), // TODO: - 수정 필요 @@ -145,11 +168,6 @@ final class EditPageCell: UITableViewCell { let attachment = findAttachment(by: media) attachment?.configure(with: url) } - private func saveContents() { - guard let textStorage else { return } - - input.send(.didEditPage(attributedText: textStorage)) - } /// Text에서 특정 Attachment를 찾아서 적용합니다. private func findAttachment( by media: MediaDescription @@ -167,49 +185,6 @@ final class EditPageCell: UITableViewCell { } return attachment } - /// Text와 Attachment 정보를 하나의 문자열로 조합합니다. - private func mergeStorageInformation( - text: String, - attachmentMetaData: [Int: MediaDescription] - ) -> NSAttributedString { - let mutableAttributedString = NSMutableAttributedString(string: text) - attachmentMetaData.forEach { location, description in - let range = NSRange(location: location, length: 1) - let mediaAttachment = MediaAttachment( - view: MHPolaroidPhotoView(), // TODO: - 이거 바꿔줘야함... - description: description - ) - input.send(.didRequestMediaDataForData(media: description)) - let attachmentString = NSAttributedString(attachment: mediaAttachment) - // Placeholder(공백) 교체 - mutableAttributedString.replaceCharacters(in: range, with: attachmentString) - } - - mutableAttributedString.addAttributes(defaultAttributes, - range: NSRange(location: 0, length: mutableAttributedString.length)) - - return mutableAttributedString - } - /// TextView의 높이가 적절한지 확인합니다. - private func isAcceptableHeight( - _ textStorage: NSTextStorage, - shouldChangeTextIn range: NSRange, - replacementText attributedText: NSAttributedString - ) -> Bool { - let updatedText = NSMutableAttributedString(attributedString: textStorage) - let horizontalInset = textView.textContainerInset.left + textView.textContainerInset.right - let verticalInset = textView.textContainerInset.top + textView.textContainerInset.bottom - let textViewWidth = textView.bounds.width - horizontalInset - let textViewHight = textView.bounds.height - verticalInset - let temporaryTextView = UITextView( - frame: CGRect(x: 0, y: 0, width: textViewWidth, height: .greatestFiniteMagnitude) - ) - updatedText.replaceCharacters(in: range, with: attributedText) - temporaryTextView.attributedText = updatedText - temporaryTextView.sizeToFit() - - return temporaryTextView.contentSize.height <= textViewHight - } } // MARK: - MediaAttachmentDataSource @@ -242,6 +217,26 @@ extension EditPageCell: UITextViewDelegate { func textViewDidBeginEditing(_ textView: UITextView) { input.send(.didBeginEditingPage) } + /// TextView의 높이가 적절한지 확인합니다. + private func isAcceptableHeight( + _ textStorage: NSTextStorage, + shouldChangeTextIn range: NSRange, + replacementText attributedText: NSAttributedString + ) -> Bool { + let updatedText = NSMutableAttributedString(attributedString: textStorage) + let horizontalInset = textView.textContainerInset.left + textView.textContainerInset.right + let verticalInset = textView.textContainerInset.top + textView.textContainerInset.bottom + let textViewWidth = textView.bounds.width - horizontalInset + let textViewHight = textView.bounds.height - verticalInset + let temporaryTextView = UITextView( + frame: CGRect(x: 0, y: 0, width: textViewWidth, height: .greatestFiniteMagnitude) + ) + updatedText.replaceCharacters(in: range, with: attributedText) + temporaryTextView.attributedText = updatedText + temporaryTextView.sizeToFit() + + return temporaryTextView.contentSize.height <= textViewHight + } } // MARK: - NSTextStorageDelegate @@ -300,7 +295,7 @@ extension EditPageCell: @preconcurrency NSTextStorageDelegate { // TODO: - 좀더 우아하게 처리하기? 미리 알려주는 로직으로... 개선필요 textStorage.deleteCharacters(in: editedRange) } - saveContents() + input.send(.didEditPage(attributedText: textStorage)) } // 그곳에 Attachment가 있는지 확인합니다. private func attachmentAt(_ index: Int) -> MediaAttachment? { From 201c1cfec89ce2511ce2575e9f31a9ab27718ac2 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 10:07:17 +0900 Subject: [PATCH 56/60] =?UTF-8?q?refactor:=20MediaAttachment=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditBook/View/MediaAttachable.swift | 7 ++++ .../EditBook/View/MediaAttachment.swift | 35 ------------------- .../View/MediaAttachmentViewProvider.swift | 31 ++++++++++++++++ 3 files changed, 38 insertions(+), 35 deletions(-) create mode 100644 MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachable.swift create mode 100644 MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachmentViewProvider.swift diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachable.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachable.swift new file mode 100644 index 00000000..d6cdc5ef --- /dev/null +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachable.swift @@ -0,0 +1,7 @@ +import MHFoundation +import MHDomain + +protocol MediaAttachable { + func configureSource(with mediaDescription: MediaDescription, data: Data) + func configureSource(with mediaDescription: MediaDescription, url: URL) +} diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift index 10be10ea..fd62c0fc 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachment.swift @@ -63,38 +63,3 @@ final class MediaAttachment: NSTextAttachment { view.configureSource(with: mediaDescription, url: url) } } - -class MediaAttachmentViewProvider: NSTextAttachmentViewProvider { - // MARK: - Property - var type: MediaType? - private var height: CGFloat { - switch type { // TODO: - 조정 필요 - case .image: - 300 - case .video: - 200 - case .audio: - 100 - case nil: - 10 - default: - 100 - } - } - - override func attachmentBounds( - for attributes: [NSAttributedString.Key: Any], - location: NSTextLocation, - textContainer: NSTextContainer?, - proposedLineFragment: CGRect, - position: CGPoint - ) -> CGRect { - return CGRect(x: 0, y: 0, width: proposedLineFragment.width, height: height) - } -} - -// MARK: - MediaAttachable -protocol MediaAttachable { - func configureSource(with mediaDescription: MediaDescription, data: Data) - func configureSource(with mediaDescription: MediaDescription, url: URL) -} diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachmentViewProvider.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachmentViewProvider.swift new file mode 100644 index 00000000..01e87a74 --- /dev/null +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/MediaAttachmentViewProvider.swift @@ -0,0 +1,31 @@ +import UIKit +import MHDomain + +final class MediaAttachmentViewProvider: NSTextAttachmentViewProvider { + // MARK: - Property + var type: MediaType? + private var height: CGFloat { + switch type { // TODO: - 조정 필요 + case .image: + 300 + case .video: + 200 + case .audio: + 100 + case nil: + 10 + default: + 100 + } + } + + override func attachmentBounds( + for attributes: [NSAttributedString.Key: Any], + location: NSTextLocation, + textContainer: NSTextContainer?, + proposedLineFragment: CGRect, + position: CGPoint + ) -> CGRect { + return CGRect(x: 0, y: 0, width: proposedLineFragment.width, height: height) + } +} From 61a2d60415c1fbe7578921eb86c4832a6101073b Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 10:16:29 +0900 Subject: [PATCH 57/60] =?UTF-8?q?refactor:=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9E=A0=EC=9E=AC=EC=A0=81=20=EC=9C=84=ED=97=98=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHPresentation/Source/EditBook/View/EditPageCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift index 58026092..eae071b7 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditPageCell.swift @@ -127,10 +127,10 @@ final class EditPageCell: UITableViewCell { view: MHPolaroidPhotoView(), // TODO: - 이거 바꿔줘야함... description: description ) - input.send(.didRequestMediaDataForData(media: description)) let attachmentString = NSAttributedString(attachment: mediaAttachment) // Placeholder(공백) 교체 mutableAttributedString.replaceCharacters(in: range, with: attachmentString) + input.send(.didRequestMediaDataForData(media: description)) } mutableAttributedString.addAttributes(defaultAttributes, From 2b6bfb60a367e8c0bfe830b60204d51318ede904 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 11:51:41 +0900 Subject: [PATCH 58/60] =?UTF-8?q?feat:=20=EC=B7=A8=EC=86=8C=EC=8B=9C=20?= =?UTF-8?q?=EC=A7=80=EC=9A=B0=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHApplication/Source/App/SceneDelegate.swift | 5 +++-- .../MHDomain/UseCase/DefaultBookUseCase.swift | 5 ++++- .../MHDomain/UseCase/DefaultMediaUseCase.swift | 7 +++++-- .../MHDomain/UseCase/Interface/MediaUseCase.swift | 4 +++- .../BookCreation/CreateBookViewController.swift | 2 +- .../Source/EditBook/View/EditBookViewController.swift | 1 + .../Source/EditBook/ViewModel/EditBookViewModel.swift | 11 +++++++++++ 7 files changed, 28 insertions(+), 7 deletions(-) diff --git a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift index eca10b8c..83a59fe4 100644 --- a/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift +++ b/MemorialHouse/MHApplication/MHApplication/Source/App/SceneDelegate.swift @@ -176,9 +176,11 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { // MARK: - Book UseCase let bookRepository = try DIContainer.shared.resolve(BookRepository.self) + let mediaRepository = try DIContainer.shared.resolve(MediaRepository.self) DIContainer.shared.register( CreateBookUseCase.self, - object: DefaultCreateBookUseCase(repository: bookRepository) + object: DefaultCreateBookUseCase(repository: bookRepository, + mediaRepository: mediaRepository) ) DIContainer.shared.register( FetchBookUseCase.self, @@ -204,7 +206,6 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { object: DefaultUpdateBookCoverUseCase(repository: bookCoverRepository) ) // MARK: - EditBook UseCase - let mediaRepository = try DIContainer.shared.resolve(MediaRepository.self) DIContainer.shared.register( PersistentlyStoreMediaUseCase.self, object: DefaultPersistentlyStoreMediaUseCase(repository: mediaRepository) diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift index 4b35c0ab..ea7d0df9 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultBookUseCase.swift @@ -2,13 +2,16 @@ import MHFoundation public struct DefaultCreateBookUseCase: CreateBookUseCase { private let repository: BookRepository + private let mediaRepository: MediaRepository - public init(repository: BookRepository) { + public init(repository: BookRepository, mediaRepository: MediaRepository) { self.repository = repository + self.mediaRepository = mediaRepository } public func execute(book: Book) async throws { try await repository.create(book: book).get() + try await mediaRepository.createSnapshot(for: [], in: book.id).get() } } diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift index a3076ec6..0f0241ed 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/DefaultMediaUseCase.swift @@ -77,8 +77,11 @@ public struct DefaultPersistentlyStoreMediaUseCase: PersistentlyStoreMediaUseCas public func execute(to bookID: UUID) async throws { // TODO: - 없어질 로직 try await repository.moveAllTemporaryMedia(to: bookID).get() } - public func execute(to bookID: UUID, mediaList: [MediaDescription]) async throws { - try await repository.createSnapshot(for: mediaList, in: bookID).get() + public func execute(to bookID: UUID, mediaList: [MediaDescription]?) async throws { + if let mediaList { + try await repository.createSnapshot(for: mediaList, in: bookID).get() + } + try await repository.deleteMediaBySnapshot(for: bookID).get() } } diff --git a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift index fc43406c..cb434449 100644 --- a/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift +++ b/MemorialHouse/MHDomain/MHDomain/UseCase/Interface/MediaUseCase.swift @@ -17,5 +17,7 @@ public protocol DeleteMediaUseCase: Sendable { public protocol PersistentlyStoreMediaUseCase: Sendable { @available(*, deprecated, message: "temp를 더이상 사용하지 않습니다.") func execute(to bookID: UUID) async throws // TODO: - 없애야함 - func execute(to bookID: UUID, mediaList: [MediaDescription]) async throws + /// mediaList가 없을 경우 현재 디렉토리의 스냅샷 기준으로 저장합니다. + /// mediaList가 있을 경우 해당 목록을 기준으로 저장합니다. + func execute(to bookID: UUID, mediaList: [MediaDescription]?) async throws } diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift index da0fd209..cb8baf44 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift @@ -217,7 +217,7 @@ final class CreateBookViewController: UIViewController { Task { guard let editBookViewModelFactory = try? DIContainer.shared.resolve(EditBookViewModelFactory.self) else { return } let book = Book(id: .init(), title: "HIHI", pages: [.init(metadata: [:], text: "")]) - try? await DIContainer.shared.resolve(BookRepository.self).create(book: book) + try? await DIContainer.shared.resolve(CreateBookUseCase.self).execute(book: book) let viewModel = editBookViewModelFactory.make(bookID: book.id) self?.navigationController?.pushViewController( EditBookViewController(viewModel: viewModel), diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index fafd0e0f..13479958 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -132,6 +132,7 @@ final class EditBookViewController: UIViewController { ) alert.addAction(UIAlertAction(title: "취소", style: .cancel)) alert.addAction(UIAlertAction(title: "확인", style: .default) { _ in + self?.input.send(.didCancelButtonTapped) self?.navigationController?.popViewController(animated: true) }) self?.present(alert, animated: true) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift index 7b89b4a9..fce00d40 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/ViewModel/EditBookViewModel.swift @@ -11,6 +11,7 @@ final class EditBookViewModel: ViewModelType { case didAddMediaInURL(type: MediaType, url: URL) case addPageButtonTapped case didSaveButtonTapped + case didCancelButtonTapped } enum Output { case updateViewController(title: String) @@ -64,6 +65,8 @@ final class EditBookViewModel: ViewModelType { self?.addEmptyPage() case .didSaveButtonTapped: Task { await self?.saveMediaAll() } + case .didCancelButtonTapped: + Task { await self?.revokeMediaAll() } } }.store(in: &cancellables) @@ -139,6 +142,14 @@ final class EditBookViewModel: ViewModelType { MHLogger.error(error.localizedDescription + #function) } } + private func revokeMediaAll() async { + do { + try await storeMediaUseCase.execute(to: bookID, mediaList: nil) + } catch { + output.send(.error(message: "저장 취소하는데 실패했습니다.")) + MHLogger.error(error.localizedDescription + #function) + } + } // MARK: - Method For ViewController func numberOfPages() -> Int { From 604cbc9e1c7a52c5ff1e122bfe720e2bf5a15967 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 11:58:14 +0900 Subject: [PATCH 59/60] =?UTF-8?q?chore:=20conflict=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MHPresentation/Source/Home/HomeViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift index b44beab5..ee880f4d 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/Home/HomeViewController.swift @@ -170,7 +170,7 @@ public final class HomeViewController: UIViewController { } private func moveMakingBookViewController() { - let bookCreationViewController = BookCreationViewController(viewModel: BookCreationViewModel()) + let bookCreationViewController = CreateBookViewController(viewModel: CreateBookViewModel()) navigationController?.pushViewController(bookCreationViewController, animated: true) } From 8b263d2b4fe7c0c189a615bbbead1dc0aa01ebf5 Mon Sep 17 00:00:00 2001 From: iceHood Date: Mon, 2 Dec 2024 12:10:16 +0900 Subject: [PATCH 60/60] =?UTF-8?q?chore:=20todo=20=EB=82=A8=EA=B8=B0?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Source/BookCreation/CreateBookViewController.swift | 1 + .../Source/EditBook/View/EditBookViewController.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift index 863ec9fe..569954f0 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/BookCreation/CreateBookViewController.swift @@ -76,6 +76,7 @@ final class CreateBookViewController: UIViewController { return shadowLayer }() + // TODO: - 뷰모델 개선 필요 @Published private var viewModel: CreateBookViewModel private var cancellables: Set = [] diff --git a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift index 13479958..50c270b6 100644 --- a/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift +++ b/MemorialHouse/MHPresentation/MHPresentation/Source/EditBook/View/EditBookViewController.swift @@ -2,6 +2,7 @@ import UIKit import MHCore import Combine +// TODO: - 페이지 없애는 기능 추가 final class EditBookViewController: UIViewController { // MARK: - Constant static let buttonBottomConstant: CGFloat = -20