본문 바로가기

Project/30MinRead

📌 Swift에서 독서 계획 내에 독서 메모를 CRUD하는 방법 (CoreData + Combine + MVVM)

🔹 목표

독서 계획(ReadItem) 안에 여러 개의 독서 메모(ReadMemo)를 추가하고 관리하는 기능을 CoreData의 관계(Relationship) 를 활용하여 저장하고, Combine + MVVM 패턴으로 CRUD 기능을 구현한다.

 

🔹 1. CoreData에 ReadItem ↔ ReadMemo 관계 설정

✅ ReadItem (독서 계획)

  • id: UUID
  • title: String
  • startDate: Date
  • endDate: Date
  • memos: ReadMemo와 1:N 관계 (📌 중요!)

✅ ReadMemo (독서 메모)

  • id: UUID
  • parentID: UUID (ReadItem과 연결)
  • memo: String
  • page: Int
  • readItem: ReadItem과 관계 설정 (📌 중요!)

💡 CoreData 관계 설정 방법

  1. ReadItem ↔ ReadMemo 사이 1:N 관계 설정
    • ReadItem이 여러 개의 ReadMemo를 가질 수 있도록 설정
  2. ReadMemo의 readItem 속성을 Optional X, Delete Rule = Cascade
    • 부모 ReadItem이 삭제되면, 관련된 ReadMemo도 자동 삭제

CoreData 모델을 설정한 후, NSManagedObject 클래스를 생성해야 함!
(Editor > Create NSManagedObject Subclass 사용)

 

 

🔹 2. 설계 요약 흐름 

 

  • DetailViewController → AddMemoViewController로 ReadItemModel을 전달
  • AddMemoViewController가 AddMemoViewModel에 ReadItemModel을 전달
  • AddMemoViewModel에서 ReadItemModel이 설정되면 자동으로 CoreData의 ReadItem으로 변환
  • 변환된 ReadItem을 selectedReadItem에 저장
  • 저장 버튼 누르면 selectedReadItem을 이용해 CoreData에 저장

👉 결론: ReadItemModel을 ReadItem으로 변환하는 과정이 자동으로 실행되도록 하고 싶음

 

🔹 3. 설계 시작~

// ✅ ReadMemoModel: 독서 메모 클래스 
class ReadMemoModel {
    var id: String        // ex) "readItemID_memoID"
    var memo: String
    var page: Int
    
    init(parentID: UUID, memo: String, page: Int) {
        self.id = "\(parentID.uuidString)_\(UUID().uuidString)"
        self.memo = memo
        self.page = page
    }
}

 

 

 

📍 DetailViewController 에서 AddMemoViewController로 화면 전환할 때 ReadItemModel 타입의 readItem(독서계획)을 전달한다.

이러면 이 데이터를 AddMemoViewModel 클래스 내에 readItemModel 프로퍼티에 전달한다. 

그러면 프로퍼티 감시자를 통해 fetchReadItem() 메서드를 실행한다. 

이를 통해 ReadItemModel 타입의 데이터를 코어데이터의 엔티티 타입에 대한 ReadItem 타입으로 반환한다. 

이 반환된 데이터를 selectedReadItem에 할당한다.

이를 갖고, createReadMemo 메서드에서 독서 메모의 id를 설정할 때 사용할 수 있게 한다. 

// ✅ AddMemoViewModel

class AddMemoViewModel {
    
    @Published var readItemModel: ReadItemModel? {
        didSet {
            fetchReadItem()    // 값이 바뀔 때마다 자동 변환
        }
    }
    @Published var selectedReadItem: ReadItem?  // CoreData의 ReadItem
    @Published var newReadMemo: ReadMemoModel = ReadMemoModel(
        parentID: UUID(),
        memo: "",
        page: 0)
    @Published var readMemos: [ReadMemoModel] = []
    @Published var errorMessage: String?
    
    private var cancellables: Set<AnyCancellable> = []
    let coredataManager = CoreDataManager.shared
    
    
    
    // MARK: - Function: ReadItemModel 타입의 데이터를 ReadItem (엔티티) 타입으로 변환
    func fetchReadItem() {
        guard let model = readItemModel else { return }
        if let readItem = coredataManager.fetchReadItem(by: model.id) {
            self.selectedReadItem = readItem
            print("✅ CoreData에서 ReadItem 변환 완료: \(readItem.id ?? UUID())")
        } else {
            print("❌ CoreData에서 ReadItem을 찾을 수 없습니다.")
        }
    }
    
    
    
    
    // MARK: - Functions: CRUD
    // Create
    func createNewReadMemo(_ memo: ReadMemoModel) {
        print("🧑‍💻 AddMemoViewModel: 새로운 독서 메모 저장 요청")
        guard let parent = selectedReadItem else {
            print("❌ 저장 실패: selectedReadItem이 없습니다.")
            return
        }
        
        coredataManager.createReadMemo(memo, for: parent)
            .sink { completion in
                switch completion {
                case .finished:
                    print("✅ AddMemoViewModel: 독서 메모 저장 완료되었습니다.!")
                case .failure(let error):
                    print("❌ AddMemoViewModel: 저장 실패 \(error.localizedDescription)")
                    self.errorMessage = error.localizedDescription
                }
            } receiveValue: { [weak self] newMemo in
                print("🧑‍💻 AddMemoViewModel: 저장된 독서 메모 확인")
                print("   - ID: \(newMemo.id)")
                print("   - Memo: \(newMemo.memo)")
                print("   - Page: \(newMemo.page)")
                self?.newReadMemo = newMemo
                self?.readMemos.append(newMemo)
            }
            .store(in: &cancellables)
        
    }

 

 

📍 readItemModel을 viewModel에 전달하면 자동으로 변환이 이루어집니다. 

class AddMemoViewController: UIViewController {
    
    // MARK: - Variables
    private var viewModel: AddMemoViewModel = AddMemoViewModel()
    private var cancellables: Set<AnyCancellable> = []
    
    private var mode: AddMemoMode = .create
    var readItem: ReadItemModel
    var readMemo: ReadMemoModel
    
    ...
    
    // ✅ MARK: - Init
    init(mode: AddMemoMode, readItem: ReadItemModel, readMemo: ReadMemoModel? = nil) {
        self.mode = mode
        self.readItem = readItem
        self.viewModel.readItemModel = readItem
        
        switch mode {
        case .create:
            self.readMemo = readMemo ?? ReadMemoModel(parentID: readItem.id, memo: "", page: 0)
        case .edit:
            self.readMemo = readMemo!
        }
    
        self.viewModel.newReadMemo = self.readMemo
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    ... 
    
    // ✅ MARK: - 저장메서드
        @objc private func addMemo() {
        print("✅ addMemoButton - called ")
        
        switch mode {
        case .create:
            viewModel.createNewReadMemo(viewModel.newReadMemo)
            print("🎊 새로운 메모 저장: \(viewModel.newReadMemo)")
            
        case .edit:
            print("😘 수정된 메모 저장: \(viewModel.newReadMemo)")
        }
        
        dismiss(animated: true )
    }