Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Book, Page 등 Entity, Storage, Repository, FileManager 생성 #82

Merged
merged 48 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
324496a
feat: Page로직 구현을 위한 엔티티 추가
iceHood Nov 25, 2024
2d77243
chore: Page 린트 수정
iceHood Nov 26, 2024
0095076
feat: Book 엔티티 만들기
iceHood Nov 26, 2024
ec3fcc9
refactor: Entity 기존 컨벤션에 맞게 수정
iceHood Nov 26, 2024
0da53c1
refactor: MediaType이 String을 rawValue로 가짐
iceHood Nov 26, 2024
cad4f14
feat: 추후 CoreData를 위한 DTO추상화
iceHood Nov 26, 2024
4404356
feat: Book, Page 스토리지 CRUD 프로토콜 생성
iceHood Nov 26, 2024
1c0745d
feat: CoreData에 Book추가
iceHood Nov 26, 2024
08781ec
feat: BookRepository 생성
iceHood Nov 26, 2024
26d3b48
refactor: PageStroage 없어도 될듯
iceHood Nov 26, 2024
d680ab2
feat: 파일 저장 Storage-MHFileManager생성
iceHood Nov 26, 2024
464ffc4
feat: BookStroage생성
iceHood Nov 26, 2024
fbc49b2
feat: MHError유형 추가
iceHood Nov 26, 2024
4b1329c
refactor: Book을 id로 가져오게 설정
iceHood Nov 26, 2024
2ee6924
feat: Test임시 추가
iceHood Nov 26, 2024
ead3b39
feat: BookStorageTest 작성
iceHood Nov 26, 2024
1204d36
chore: 린트 경고 수정
iceHood Nov 26, 2024
41fa8dc
reafactor: 리뷰 반영
iceHood Nov 26, 2024
73e45c7
refactor: Test가 동작하도록 수정
iceHood Nov 26, 2024
5c7b869
refactor: 결국 갖은 오류끝에 Data로 변경
iceHood Nov 26, 2024
4f6bc8c
feat: FIleManager테스트 추가
iceHood Nov 26, 2024
d4a6e59
feat: MediaRepository 프로토콜 추가
iceHood Nov 26, 2024
291f971
Merge branch 'develop' into feature/creat-book-page-domain
iceHood Nov 27, 2024
0109995
feat: dto 매퍼 이름 수정
iceHood Nov 27, 2024
10f59e4
refactor: FileStroage필요한 함수 및 설명 추가
iceHood Nov 27, 2024
768f42f
feat: FileManage 신규 함수 추가
iceHood Nov 27, 2024
56e89d3
feat: LocalMediaRepository 작성
iceHood Nov 27, 2024
d9f5a6a
feat: 에러 추가
iceHood Nov 27, 2024
0518b74
refactor: Repository에서 result로 반환하도록 설정
iceHood Nov 27, 2024
e251e11
refactor: 새로운 로직에 대응
iceHood Nov 27, 2024
64c4887
refactor: Decodable대신 CoreData사용
iceHood Nov 27, 2024
c7a74bd
chore: Lint경고 안나오도록 수정
iceHood Nov 27, 2024
67e5b4f
Merge branch 'develop' into feature/creat-book-page-domain
iceHood Nov 27, 2024
7e6a62e
refactor: DTO에 public init 추가
iceHood Nov 27, 2024
551dd18
refactor: MHError -> MHCoreError / MHDataError
iceHood Nov 27, 2024
ab06643
chore: Lint경고 수정
iceHood Nov 27, 2024
02d6543
feat: MediaDescription에 attribute추가
iceHood Nov 27, 2024
731c81d
fix: CoreData동시성 문제 해결
iceHood Nov 27, 2024
b8abbc9
refactor: 에러 유형 추가/구체화
iceHood Nov 27, 2024
c23d906
refactor: Error유형 변경
iceHood Nov 27, 2024
88e0341
refactor: 쓸모없는 strong self삭제
iceHood Nov 27, 2024
5a4f073
feat: CoreDataBookCover도 스레드 세이프...
iceHood Nov 27, 2024
42332f7
chore: 잘못 올라간거 삭제
iceHood Nov 27, 2024
3ee147b
Merge branch 'develop' into feature/creat-book-page-domain
iceHood Nov 27, 2024
d466ea7
refactor: BookCoverDTO도 다른 것에 컨벤션 맞춤
iceHood Nov 27, 2024
07ea62f
chore: 린트 수정
iceHood Nov 27, 2024
1ab46a8
Merge branch 'develop' into feature/creat-book-page-domain
iceHood Nov 27, 2024
3f198e9
refactor: 코어오류 따르도록 수정
iceHood Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions MemorialHouse/MHCore/MHCore/MHError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,37 @@ import Foundation
public enum MHError: Error, CustomStringConvertible, Equatable {
case DIContainerResolveFailure(key: String)
case convertDTOFailure
case fetchFaliure
case findEntityFailure
case saveContextFailure
case directorySettingFailure
case fileCreationFailure
case fileReadingFailure
case fileDeletionFailure
case fileMovingFailure

Copy link
Collaborator

Choose a reason for hiding this comment

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

P2 질문
파일과 관련된 에러처리 열거형은 따로 정의를 해두는 건 어떨까요 ?
모든 에러를 여기서 감당하면 파일의 길이가 길어질 거 같은데 괜찮을까요 ?!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

동의하는 부분입니다... 흠... 그러면 저희 모듈별로 오류를 만들어 놓는 것은 어떨까요? (너무 구린가)

Copy link
Collaborator

Choose a reason for hiding this comment

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

전 모듈별로 에러를 만들어서 처리하는거 좋다고 생각합니다 .ᐟ.ᐟ

Copy link
Collaborator

Choose a reason for hiding this comment

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

임정합니더 오히려 좋을 거 같아요

public var description: String {
switch self {
case .DIContainerResolveFailure(let key):
"\(key)에 대한 dependency resolve 실패"
case .convertDTOFailure:
"Entity에 대한 DTO 변환 실패"
case .fetchFaliure:
"Entity Fetch 실패"
case .findEntityFailure:
"Entity 찾기 실패"
case .saveContextFailure:
"Update된 Context 저장 실패"
case .directorySettingFailure:
"디렉토리 설정 실패"
case .fileCreationFailure:
"파일 생성 실패"
case .fileReadingFailure:
"파일 읽기 실패"
case .fileDeletionFailure:
"파일 삭제 실패"
case .fileMovingFailure:
"파일 이동 실패"
}
}
}
14 changes: 14 additions & 0 deletions MemorialHouse/MHData/MHData/DTO/BookDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import MHFoundation
import MHDomain

public struct BookDTO {
let id: UUID
let pages: [PageDTO]

Copy link
Collaborator

Choose a reason for hiding this comment

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

public init 없이 잘 동작하나요 ??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Internal하게 사용돼서 아직 오류가 안나온 것같습니다! 혹시 모르니 넣어 놓을께욤!

func toBook() -> Book {
return Book(
id: self.id,
pages: self.pages.map { $0.toPage() }
)
}
}
16 changes: 16 additions & 0 deletions MemorialHouse/MHData/MHData/DTO/MediaDescriptionDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import MHFoundation
import MHDomain

public struct MediaDescriptionDTO: Codable {
let id: UUID
let type: String

func toMediaDescription() -> MediaDescription? {
guard let type = MediaType(rawValue: self.type) else { return nil }

return MediaDescription(
id: self.id,
type: type
)
}
}
19 changes: 19 additions & 0 deletions MemorialHouse/MHData/MHData/DTO/PageDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import MHFoundation
import MHDomain

public struct PageDTO: Codable {
let id: UUID
let metadata: [Int: MediaDescriptionDTO]
let text: String

func toPage() -> Page {
let metadata = self.metadata
.compactMapValues { $0.toMediaDescription() }

return Page(
id: self.id,
metadata: metadata,
text: self.text
)
}
}
9 changes: 9 additions & 0 deletions MemorialHouse/MHData/MHData/LocalStorage/BookStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import MHFoundation
import MHCore

public protocol BookStorage {
func create(data: BookDTO) async -> Result<Void, MHError>
func fetch(with id: UUID) async -> Result<BookDTO, MHError>
func update(with id: UUID, data: BookDTO) async -> Result<Void, MHError>
func delete(with id: UUID) async -> Result<Void, MHError>
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ extension CoreDataBookCoverStorage: BookCoverStorage {
return .success(result)
} catch {
MHLogger.debug("Error fetching book covers: \(error.localizedDescription)")
return .failure(.convertDTOFailure)
return .failure(.fetchFaliure)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import MHFoundation
import MHCore
import CoreData

final class CoreDataBookStorage {
private let coreDataStorage: CoreDataStorage

init(coreDataStorage: CoreDataStorage) {
self.coreDataStorage = coreDataStorage
}
}

extension CoreDataBookStorage: BookStorage {
func create(data: BookDTO) async -> Result<Void, MHError> {
let context = coreDataStorage.persistentContainer.viewContext
guard let entity = NSEntityDescription.entity(forEntityName: "BookEntity", in: context) else {
return .failure(.DIContainerResolveFailure(key: "BookEntity"))
}

let book = NSManagedObject(entity: entity, insertInto: context)
book.setValue(data.id, forKey: "id")
book.setValue(DTOPagesToCore(data.pages), forKey: "pages")

await coreDataStorage.saveContext()
return .success(())
}
func fetch(with id: UUID) async -> Result<BookDTO, MHError> {
let context = coreDataStorage.persistentContainer.viewContext

do {
guard let bookEntity = try getEntityByIdentifier(in: context, with: id)
else { return .failure(.findEntityFailure) }

guard let result = coreBookToDTO(bookEntity)
else { return .failure(.convertDTOFailure) }

return .success(result)
} catch {
MHLogger.debug("Error fetching book: \(error.localizedDescription)")
return .failure(.findEntityFailure)
}
}
func update(with id: UUID, data: BookDTO) async -> Result<Void, MHError> {
do {
let context = coreDataStorage.persistentContainer.viewContext
guard let newEntity = try getEntityByIdentifier(in: context, with: id) else {
return .failure(.findEntityFailure)
}
newEntity.setValue(data.id, forKey: "id")
newEntity.setValue(DTOPagesToCore(data.pages), forKey: "pages")

await coreDataStorage.saveContext()
return .success(())
} catch {
return .failure(.findEntityFailure)
}
}
func delete(with id: UUID) async -> Result<Void, MHError> {
do {
let context = coreDataStorage.persistentContainer.viewContext
guard let entity = try getEntityByIdentifier(in: context, with: id) else {
return .failure(.findEntityFailure)
}

context.delete(entity)

await coreDataStorage.saveContext()
return .success(())
} catch {
return .failure(.findEntityFailure)
}
}

private func getEntityByIdentifier(in context: NSManagedObjectContext, with id: UUID) throws -> BookEntity? {
let request = BookEntity.fetchRequest()

request.predicate = NSPredicate(
format: "id == %@", id as CVarArg
)

return try context.fetch(request).first
}
}

// MARK: - Mapper
extension CoreDataBookStorage {
// MARK: - Core to DTO
private func coreBookToDTO(_ book: BookEntity) -> BookDTO? {
guard let id = book.id,
let pages = try? JSONDecoder().decode([PageDTO].self, from: book.pages ?? Data())
else { return nil }

return BookDTO(
id: id,
pages: pages
)
}

// MARK: - DTO to Core
private func DTOPagesToCore(_ pages: [PageDTO]) -> Data? {
Copy link
Collaborator

Choose a reason for hiding this comment

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

메소드 첫 글자 소문자가 아니면 린트 경고가 발생했던 거 같은데
해당 메소드는 문제 없었나용 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

하하.. 이거 함 갈아엎었읍니다..

guard let data = try? JSONEncoder().encode(pages) else { return nil }
return data
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
<attribute name="imageURL" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>
</entity>
<entity name="BookEntity" representedClassName="BookEntity" syncable="YES" codeGenerationType="class">
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="pages" optional="YES" attributeType="Binary" customClassName="[PageEntity]"/>
</entity>
</model>
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import MHFoundation
import MHCore
import MHDomain

public struct MHFileManager {
private let fileManager = FileManager.default
private let 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, MHError> {
guard let directory = fileManager.urls(
for: directoryType,
in: .userDomainMask
).first?.appending(path: path)
else { return .failure(.directorySettingFailure) }

let dataPath = directory.appendingPathComponent(name)

do {
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
try data.write(to: dataPath)
return .success(())
} catch {
return .failure(.fileCreationFailure)
}
}
func read(at path: String, fileName name: String) async -> Result<Data, MHError> {
guard let directory = fileManager.urls(
for: directoryType,
in: .userDomainMask
).first?.appending(path: path)
else { return .failure(.directorySettingFailure) }

let dataPath = directory.appendingPathComponent(name)

do {
return .success(try Data(contentsOf: dataPath))
} catch {
return .failure(.fileReadingFailure)
}
}
func delete(at path: String, fileName name: String) async -> Result<Void, MHError> {
guard let directory = fileManager.urls(
for: directoryType,
in: .userDomainMask
).first?.appending(path: path)
else { return .failure(.directorySettingFailure) }

let dataPath = directory.appendingPathComponent(name)

do {
try fileManager.removeItem(at: dataPath)
return .success(())
} catch {
return .failure(.fileDeletionFailure)
}
}
func move(at path: String, fileName name: String, to newPath: String) async -> Result<Void, MHError> {
guard let originDirectory = fileManager.urls(
for: directoryType,
in: .userDomainMask
).first?.appending(path: path)
else { return .failure(.directorySettingFailure) }

let originDataPath = originDirectory.appendingPathComponent(name)

guard let newDirectory = fileManager.urls(
for: directoryType,
in: .userDomainMask
).first?.appending(path: newPath)
else { return .failure(.directorySettingFailure) }

let newDataPath = newDirectory.appendingPathComponent(name)

do {
try fileManager.createDirectory(at: newDirectory, withIntermediateDirectories: true)
try fileManager.moveItem(at: originDataPath, to: newDataPath)
return .success(())
} catch {
return .failure(.fileMovingFailure)
}
}
}
9 changes: 9 additions & 0 deletions MemorialHouse/MHData/MHData/LocalStorage/FileStorage.swift
Copy link
Collaborator

Choose a reason for hiding this comment

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

캬.. 빛 정 현.. 꼼꼼한 주석 또 한 번 제 이마를 탁치고 배우고 갑니다..

Copy link
Collaborator

Choose a reason for hiding this comment

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

ㅘㅜ 주석 깔꼼하네요

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

protocol FileStorage {
func create(at path: String, fileName name: String, data: Data) async -> Result<Void, MHError>
func read(at path: String, fileName name: String) async -> Result<Data, MHError>
func delete(at path: String, fileName name: String) async -> Result<Void, MHError>
func move(at path: String, fileName name: String, to newPath: String) async -> Result<Void, MHError>
}
61 changes: 61 additions & 0 deletions MemorialHouse/MHData/MHData/Repository/LocalBookRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import MHFoundation
import MHDomain
import MHCore

public struct LocalBookRepository: BookRepository {
private let storage: BookStorage

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

public func create(book: Book) async {
let bookDTO = mappingBookToDTO(book)

_ = await storage.create(data: bookDTO)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

질문
해당 코드를 포함한 create, update, delete 메서드에서 Result<,> 처리는 어느 부분에서 해주나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

사실 있는 코드 복붙한거라 생각 안하고 하긴했습니다 ㅋㅋㅋ
이거 result로 return해도 될 것같긴합니다.
@k2645 님! 혹시 Result로 반환 안하신 이유가 있으신가요?!

Copy link
Collaborator

Choose a reason for hiding this comment

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

엇 저도 실은.. 크게..생각을 안하고 있었던 것 같...숩다.......... UseCase도 그럼 Result로 다..넘겨줘야할까오/...? 아님 Repository부터는 throw를 해도 될 것 같기도하구요/...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Resut = Storage, Repository
throws = ViewModel, UseCase

이렇게 하면 되겠죠 ?!

public func fetch(bookID id: UUID) async -> Book? {
let result = await storage.fetch(with: id)

switch result {
case .success(let bookDTO):
return bookDTO.toBook()
case .failure(let failure):
MHLogger.debug("\(failure.description)")
}

return nil
}
public func update(bookID id: UUID, to book: Book) async {
let bookDTO = mappingBookToDTO(book)

_ = await storage.update(with: id, data: bookDTO)
}
public func delete(bookID id: UUID) async {
_ = await storage.delete(with: id)
}

private func mappingBookToDTO(_ book: Book) -> BookDTO {
let pages = book.pages.map { mappingPageToDTO($0) }
return BookDTO(
id: book.id,
pages: pages
)
}
private func mappingPageToDTO(_ page: Page) -> PageDTO {
let meatadata = page.metadata
.compactMapValues { mappingMediaDescriptionToDTO($0) }

return PageDTO(
id: page.id,
metadata: meatadata,
text: page.text
)
}
private func mappingMediaDescriptionToDTO(_ description: MediaDescription) -> MediaDescriptionDTO {
return MediaDescriptionDTO(
id: description.id,
type: description.type.rawValue
)
}
}
Loading
Loading