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 17 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
15 changes: 15 additions & 0 deletions MemorialHouse/MHCore/MHCore/MHError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ public enum MHError: Error, CustomStringConvertible, Equatable {
case convertDTOFailure
case findEntityFailure
case saveContextFailure
case directorySettingError
case fileCreationError
case fileReadingError
case fileDeletionError
case fileMovingError
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3: description을 보니 실패와 관련된 에러같은데 위 에러들과 통일성을 주어 Failure은 어떤가욥?

Copy link
Collaborator Author

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 {
Expand All @@ -16,6 +21,16 @@ public enum MHError: Error, CustomStringConvertible, Equatable {
"Entity 찾기 실패"
case .saveContextFailure:
"Update된 Context 저장 실패"
case .directorySettingError:
"디렉토리 설정 실패"
case .fileCreationError:
"파일 생성 실패"
case .fileReadingError:
"파일 읽기 실패"
case .fileDeletionError:
"파일 삭제 실패"
case .fileMovingError:
"파일 이동 실패"
}
}
}
16 changes: 16 additions & 0 deletions MemorialHouse/MHData/MHData/DTO/BookDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import MHFoundation
import MHDomain

public struct BookDTO {
let id: UUID
let index: [Int]
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,
index: self.index,
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 {
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 {
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
@@ -0,0 +1,99 @@
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, MHCore.MHError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

P2: 아래도 마찬가지인데 Result의 MHError 타입에 MHCore는 제거해주면 좋을 것 같아용

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

자동완성의 폐혜... 감사합니다! 반영했어욤

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(data.index, forKey: "index")
book.setValue(data.pages, forKey: "pages")

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

request.predicate = NSPredicate(
format: "id LIKE %@", "\(id.uuidString)"
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

옹 Predicate 잘 되나요..?? 테스트 코드 작성해서 해당 부분이 잘 돌아가는지 확인해보는것도 좋을 것 같네욥

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

안돼서... 되게 바꿨습니다 ㅋㅋ



Check warning on line 36 in MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Vertical Whitespace Violation: Limit vertical whitespace to a single empty line; currently 2 (vertical_whitespace)

Check warning on line 36 in MemorialHouse/MHData/MHData/LocalStorage/CoreData/CoreDataBookStorage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Vertical Whitespace Violation: Limit vertical whitespace to a single empty line; currently 2 (vertical_whitespace)
do {
let bookEntity = try context.fetch(request)
guard let result = bookEntity.first?.toBookDTO()
else { throw MHError.convertDTOFailure }

return .success(result)
} catch {
MHLogger.debug("Error fetching book covers: \(error.localizedDescription)")
return .failure(.convertDTOFailure)
}
}
func update(with id: UUID, data: BookDTO) async -> Result<Void, MHCore.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(data.index, forKey: "index")
newEntity.setValue(data.pages, forKey: "pages")

await coreDataStorage.saveContext()
return .success(())
} catch {
return .failure(.findEntityFailure)
}
}
func delete(with id: UUID) async -> Result<Void, MHCore.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()

return try context.fetch(request).first(where: { $0.id == id })
}
}

extension BookEntity {
func toBookDTO() -> BookDTO? {
guard let id = self.id,
let index = self.index,
let pages = self.pages else { return nil }

return BookDTO(
id: id,
index: index,
pages: pages
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24A348" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24A348" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="BookCoverEntity" representedClassName="BookCoverEntity" syncable="YES" codeGenerationType="class">
<attribute name="category" optional="YES" attributeType="String"/>
<attribute name="color" optional="YES" attributeType="String" customClassName="BookColor"/>
Expand All @@ -8,4 +8,9 @@
<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="index" optional="YES" attributeType="Transformable" customClassName="[Int]"/>
<attribute name="pages" optional="YES" attributeType="Transformable" customClassName="[PageDTO]"/>
</entity>
</model>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import MHFoundation
import MHCore
import MHDomain

public struct MHFileManager {
private let fileManager = FileManager.default
private let directoryType: FileManager.SearchPathDirectory
}

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(.directorySettingError) }

let dataPath = directory.appendingPathComponent(name)

do {
try fileManager.createDirectory(at: directory, withIntermediateDirectories: false)
try data.write(to: dataPath)
return .success(())
} catch {
return .failure(.fileCreationError)
}
}
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(.directorySettingError) }

let dataPath = directory.appendingPathComponent(name)

do {
return .success(try Data(contentsOf: dataPath))
} catch {
return .failure(.fileReadingError)
}
}
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(.directorySettingError) }

let dataPath = directory.appendingPathComponent(name)

do {
try fileManager.removeItem(at: dataPath)
return .success(())
} catch {
return .failure(.fileDeletionError)
}
}
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(.directorySettingError) }

let originDataPath = originDirectory.appendingPathComponent(name)

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

let newDataPath = newDirectory.appendingPathComponent(name)

do {
try fileManager.moveItem(at: originDataPath, to: newDataPath)
return .success(())
} catch {
return .failure(.fileMovingError)
}
}
}
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>
}
62 changes: 62 additions & 0 deletions MemorialHouse/MHData/MHData/Repository/LocalBookRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 fetchBook(with 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(id: UUID, book: Book) async {
let bookDTO = mappingBookToDTO(book)

_ = await storage.update(with: id, data: bookDTO)
}
public func deleteBook(_ 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,
index: book.index,
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