Skip to content

Commit

Permalink
Merge pull request #113 from boostcampwm-2024/feature/edit-book-logic
Browse files Browse the repository at this point in the history
책 수정 화면 로직 구현
  • Loading branch information
iceHood authored Dec 2, 2024
2 parents a505387 + 8b263d2 commit 541c1b0
Show file tree
Hide file tree
Showing 23 changed files with 1,038 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,18 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
MHLogger.error("\(error.localizedDescription)")
}
}

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

let coreDataStorage = try DIContainer.shared.resolve(CoreDataStorage.self)
DIContainer.shared.register(
CoreDataBookCoverStorage.self,
object: CoreDataBookCoverStorage(coreDataStorage: coreDataStorage)
)
DIContainer.shared.register(
CoreDataBookStorage.self,
object: CoreDataBookStorage(coreDataStorage: coreDataStorage)
)
DIContainer.shared.register(
BookCategoryStorage.self,
object: CoreDataBookCategoryStorage(coreDataStorage: coreDataStorage)
Expand All @@ -101,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 {
Expand All @@ -125,6 +136,11 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
BookRepository.self,
object: LocalBookRepository(storage: bookStorage)
)
let fileManager = try DIContainer.shared.resolve(MHFileManager.self)
DIContainer.shared.register(
MediaRepository.self,
object: LocalMediaRepository(storage: fileManager)
)
}

private func registerUseCaseDependency() throws {
Expand Down Expand Up @@ -160,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,
Expand Down Expand Up @@ -191,6 +209,23 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
DeleteBookCoverUseCase.self,
object: DefaultDeleteBookCoverUseCase(repository: bookCoverRepository)
)
// MARK: - EditBook UseCase
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 {
Expand Down Expand Up @@ -243,5 +278,23 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
ReadPageViewModelFactory.self,
object: ReadPageViewModelFactory()
)

// MARK: - EditBook ViewModel
let updateBookUseCase = try DIContainer.shared.resolve(UpdateBookUseCase.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)
DIContainer.shared.register(
EditBookViewModelFactory.self,
object: EditBookViewModelFactory(
fetchBookUseCase: fetchBookUseCase,
updateBookUseCase: updateBookUseCase,
storeMediaUseCase: storeMediaUseCase,
createMediaUseCase: createMediaUseCase,
fetchMediaUseCase: fetchMediaUseCase,
deleteMediaUseCase: deleteMediaUseCase
)
)
}
}
6 changes: 6 additions & 0 deletions MemorialHouse/MHCore/MHCore/MHDataError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public enum MHDataError: Error, CustomStringConvertible, Equatable {
case fileDeletionFailure
case fileMovingFailure
case fileNotExists
case snapshotEncodingFailure
case snapshotDecodingFailure
case generalFailure
case setUserDefaultFailure

Expand Down Expand Up @@ -48,6 +50,10 @@ public enum MHDataError: Error, CustomStringConvertible, Equatable {
"파일 이동 실패"
case .fileNotExists:
"파일이 존재하지 않습니다"
case .snapshotEncodingFailure:
"Snapshot 인코딩 실패"
case .snapshotDecodingFailure:
"Snapshot 디코딩 실패"
case .generalFailure:
"알 수 없는 에러입니다."
case .setUserDefaultFailure:
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Void, MHDataError> {
public func create(at path: String, fileName name: String, data: Data) async -> Result<Void, MHDataError> {
guard let directory = fileManager.urls(
for: directoryType,
in: .userDomainMask
Expand All @@ -22,13 +22,13 @@ 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)
}
}
func read(at path: String, fileName name: String) async -> Result<Data, MHDataError> {
public func read(at path: String, fileName name: String) async -> Result<Data, MHDataError> {
guard let directory = fileManager.urls(
for: directoryType,
in: .userDomainMask
Expand All @@ -47,7 +47,7 @@ extension MHFileManager: FileStorage {
return .failure(.fileReadingFailure)
}
}
func delete(at path: String, fileName name: String) async -> Result<Void, MHDataError> {
public func delete(at path: String, fileName name: String) async -> Result<Void, MHDataError> {
guard let directory = fileManager.urls(
for: directoryType,
in: .userDomainMask
Expand All @@ -63,7 +63,7 @@ extension MHFileManager: FileStorage {
return .failure(.fileDeletionFailure)
}
}
func copy(at url: URL, to newPath: String, newFileName name: String) async -> Result<Void, MHDataError> {
public func copy(at url: URL, to newPath: String, newFileName name: String) async -> Result<Void, MHDataError> {
let originDataPath = url

guard fileManager.fileExists(atPath: originDataPath.path) else {
Expand All @@ -86,7 +86,7 @@ extension MHFileManager: FileStorage {
return .failure(.fileMovingFailure)
}
}
func copy(at path: String, fileName name: String, to newPath: String) async -> Result<Void, MHDataError> {
public func copy(at path: String, fileName name: String, to newPath: String) async -> Result<Void, MHDataError> {
guard let originDirectory = fileManager.urls(
for: directoryType,
in: .userDomainMask
Expand Down Expand Up @@ -115,7 +115,7 @@ extension MHFileManager: FileStorage {
return .failure(.fileMovingFailure)
}
}
func move(at path: String, fileName name: String, to newPath: String) async -> Result<Void, MHDataError> {
public func move(at path: String, fileName name: String, to newPath: String) async -> Result<Void, MHDataError> {
guard let originDirectory = fileManager.urls(
for: directoryType,
in: .userDomainMask
Expand Down Expand Up @@ -144,7 +144,7 @@ extension MHFileManager: FileStorage {
return .failure(.fileMovingFailure)
}
}
func moveAll(in path: String, to newPath: String) async -> Result<Void, MHDataError> {
public func moveAll(in path: String, to newPath: String) async -> Result<Void, MHDataError> {
guard let originDirectory = fileManager.urls(
for: directoryType,
in: .userDomainMask
Expand Down Expand Up @@ -174,7 +174,7 @@ extension MHFileManager: FileStorage {
return .failure(.fileMovingFailure)
}
}
func getURL(at path: String, fileName name: String) async -> Result<URL, MHDataError> {
public func getURL(at path: String, fileName name: String) async -> Result<URL, MHDataError> {
guard let originDirectory = fileManager.urls(
for: directoryType,
in: .userDomainMask
Expand All @@ -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)
}
}
}

10 changes: 9 additions & 1 deletion MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import MHFoundation
import MHCore

public protocol FileStorage {
public protocol FileStorage: Sendable {
/// 지정된 경로에 파일을 생성합니다.
/// Documents폴더에 파일을 생성합니다.
/// 중간 경로 폴더를 자동으로 생성합니다.
Expand Down Expand Up @@ -69,4 +69,12 @@ public protocol FileStorage {
/// - name: Documents/{path}/{name} 이 파일 URL을 반환합니다. (확장자 명시 필요)
/// - Returns: 파일 URL을 반환합니다.
func getURL(at path: String, fileName name: String) async -> Result<URL, MHDataError>

/// 지정된 경로의 파일 목록을 반환합니다.
/// Documents폴더를 기준으로 파일 이름 목록을 반환합니다.
/// path는 디렉토리여야 합니다.
/// - Parameters:
/// - path: Documents/{path} 이런식으로 들어갑니다
/// - Returns: 파일 이름 목록을 반환합니다
func getFileNames(at path: String) async -> Result<[String], MHDataError>
}
62 changes: 48 additions & 14 deletions MemorialHouse/MHData/MHData/Repository/LocalMediaRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import MHCore
import AVFoundation

// TODO: nil이라면 바로 error를 return하도록 수정
public struct LocalMediaRepository: MediaRepository {
public struct LocalMediaRepository: MediaRepository, Sendable {
private let storage: FileStorage
private let temporaryPath = "temp" // TODO: - 지워질 것임!
private let snapshotFileName = ".snapshot"

public init(storage: FileStorage) {
self.storage = storage
Expand All @@ -18,34 +20,33 @@ public struct LocalMediaRepository: MediaRepository {
to bookID: UUID?
) async -> Result<Void, MHDataError> {
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<Void, MHDataError> {
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(
public func fetch(
media mediaDescription: MediaDescription,
from bookID: UUID?
) async -> Result<Data, MHDataError> {
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)
}
Expand All @@ -55,9 +56,9 @@ public struct LocalMediaRepository: MediaRepository {
at bookID: UUID?
) async -> Result<Void, MHDataError> {
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)
}
Expand All @@ -77,16 +78,49 @@ public struct LocalMediaRepository: MediaRepository {
from bookID: UUID?
) async -> Result<URL, MHDataError> {
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 moveAllTemporaryMedia(to bookID: UUID) async -> Result<Void, MHDataError> {
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<Void, MHDataError> {
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<Void, MHDataError> {
let path = bookID.uuidString

do {
let snapshotData = try await storage.read(at: path, fileName: snapshotFileName).get()
let mediaSet = Set<String>(try JSONDecoder().decode([String].self, from: snapshotData))
// snapshot 파일은 제외
let currentFiles = Set<String>(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
}
}
13 changes: 13 additions & 0 deletions MemorialHouse/MHDomain/MHDomain/Entity/MediaType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,17 @@ public enum MediaType: String, Sendable {
case image
case video
case audio

/// 기본 파일 확장자를 반환합니다.
/// 사진은 .png, 비디오는 .mp4, 오디오는 .m4a를 반환합니다.
public var defaultFileExtension: String {
switch self {
case .image:
return ".png"
case .video:
return ".mp4"
case .audio:
return ".m4a"
}
}
}
Loading

0 comments on commit 541c1b0

Please sign in to comment.