diff --git a/B.READ/B.READ/Sources/App/DIContainer/DIContainer.swift b/B.READ/B.READ/Sources/App/DIContainer/DIContainer.swift index 80c1f12c..a3920551 100644 --- a/B.READ/B.READ/Sources/App/DIContainer/DIContainer.swift +++ b/B.READ/B.READ/Sources/App/DIContainer/DIContainer.swift @@ -64,7 +64,6 @@ extension DIContainer { userInfoRepository: userInfoRepository, bookRepository: bookRepository, recordRepository: recordRepository, - quoteRepository: quoteRepository, bookService: AladinService() ), for: LibraryUseCase.self @@ -76,7 +75,8 @@ extension DIContainer { userInfoRepository: userInfoRepository, bookRepository: bookRepository, memoRepository: memoRepository, - aiService: AlanService() + aiService: AlanService(), + bookService: AladinService() ), for: MemoUseCase.self ) @@ -86,7 +86,9 @@ extension DIContainer { QuoteUseCaseImpl( userInfoRepository: userInfoRepository, quoteRepository: quoteRepository, - bookRepository: bookRepository), + bookRepository: bookRepository, + bookService: AladinService() + ), for: QuoteUseCase.self ) diff --git a/B.READ/B.READ/Sources/App/RootViewSwitcher.swift b/B.READ/B.READ/Sources/App/RootViewSwitcher.swift index 5972e9ad..601f7f46 100644 --- a/B.READ/B.READ/Sources/App/RootViewSwitcher.swift +++ b/B.READ/B.READ/Sources/App/RootViewSwitcher.swift @@ -34,8 +34,6 @@ struct RootViewSwitcher: View { LaunchScreen() .task { await DIContainer.config() - // TODO: - [더미]초기값 적용이라 마지막에 제거하기 -// await DummyService.shared.setDummy() try? await Task.sleep(for: .seconds(2)) await MainActor.run { self.isReady = true } } diff --git a/B.READ/B.READ/Sources/Domain/Entity/Book.swift b/B.READ/B.READ/Sources/Domain/Entity/Book.swift index f4c4440d..8b8dc719 100644 --- a/B.READ/B.READ/Sources/Domain/Entity/Book.swift +++ b/B.READ/B.READ/Sources/Domain/Entity/Book.swift @@ -11,7 +11,7 @@ import Foundation /// - isbn : ISBN /// - coverImg : 표지 /// - name : 제목 -/// - author : 작가 // TODO : 번역가, 옮김이 포함되는지 확인 필요 +/// - author : 작가 /// - publisher : 출판사 /// - publishedAt : 출판일 /// - totalPages: 총 페이지 diff --git a/B.READ/B.READ/Sources/Domain/UseCaseImpl/LibraryUseCaseImpl.swift b/B.READ/B.READ/Sources/Domain/UseCaseImpl/LibraryUseCaseImpl.swift index 8a70f1a4..f2ff72b8 100644 --- a/B.READ/B.READ/Sources/Domain/UseCaseImpl/LibraryUseCaseImpl.swift +++ b/B.READ/B.READ/Sources/Domain/UseCaseImpl/LibraryUseCaseImpl.swift @@ -12,22 +12,17 @@ final class LibraryUseCaseImpl: LibraryUseCase { private let userInfoRepository: UserInfoRepository private let bookRepository: BookRepository private let recordRepository: RecordRepository - // private let memoRepository: MemoRepository - private let quoteRepository: QuoteRepository - // private let noteRepository: NoteRepository private let bookService: BookService init( userInfoRepository: UserInfoRepository, bookRepository: BookRepository, recordRepository: RecordRepository, - quoteRepository: QuoteRepository, bookService: BookService ) { self.userInfoRepository = userInfoRepository self.bookRepository = bookRepository self.recordRepository = recordRepository - self.quoteRepository = quoteRepository self.bookService = bookService } @@ -97,12 +92,13 @@ final class LibraryUseCaseImpl: LibraryUseCase { // 3. record 기준으로 각각의 책정보 가져오는 걸 자식 태스크로 지정 group.addTask { do { + try Task.checkCancellation() let book = try await self.bookRepository.fetchBook(isbn: record.isbn) return (record, book) - } catch { - // TODO: - RepositoryError.dataNotFound이면 알라딘에서 책검색, 아니면 nil - print(error.localizedDescription) - return nil + + } catch RepositoryError.dataNotFound { + let book = try await self.requestBookDetail(isbn: record.isbn) + return (record, book) } } } @@ -122,7 +118,12 @@ final class LibraryUseCaseImpl: LibraryUseCase { } func deleteRecord(_ record: Record) async throws { - try await recordRepository.deleteRecord(record.id) + do { + try await recordRepository.deleteRecord(record.id) + } catch RepositoryError.dataNotFound { + // 삭제에서 이미 존재하지 않으면 무시 + print("이미 삭제된 독서 기록입니다.") + } } func loadRecentUpdatedReadingRecord(maxCount: Int) async throws -> [(Record, Book)] { diff --git a/B.READ/B.READ/Sources/Domain/UseCaseImpl/MemoUseCaseImpl.swift b/B.READ/B.READ/Sources/Domain/UseCaseImpl/MemoUseCaseImpl.swift index d6535bd1..4d34c8d2 100644 --- a/B.READ/B.READ/Sources/Domain/UseCaseImpl/MemoUseCaseImpl.swift +++ b/B.READ/B.READ/Sources/Domain/UseCaseImpl/MemoUseCaseImpl.swift @@ -13,17 +13,20 @@ final class MemoUseCaseImpl: MemoUseCase { let bookRepository: BookRepository let memoRepository: MemoRepository let aiService: AIService + private let bookService: BookService init( userInfoRepository: UserInfoRepository, bookRepository: BookRepository, memoRepository: MemoRepository, - aiService: AIService + aiService: AIService, + bookService: BookService ) { self.userInfoRepository = userInfoRepository self.bookRepository = bookRepository self.memoRepository = memoRepository self.aiService = aiService + self.bookService = bookService } func saveMemo(_ memo: Memo, in record: Record) async throws { @@ -89,7 +92,35 @@ final class MemoUseCaseImpl: MemoUseCase { } func loadBookTitle(_ isbn: String) async throws -> String { - return try await bookRepository.fetchBook(isbn: isbn).name + do { + return try await bookRepository.fetchBook(isbn: isbn).name + } catch { + // 도서 정보가 없다면 알라딘에서 검색 후 정보 생성하고 도서제목을 반환 + // 1. 알라딘에서 정보를 패치 + let bookDetail = try await bookService.fetchBookDetail(isbn: isbn) + + // 2. 패치한 정보로 엔티티 생성 + var book = Book( + isbn: bookDetail.isbn, + coverImage: nil, + name: bookDetail.title, + author: bookDetail.author, + publisher: bookDetail.publisher, + publishedAt: bookDetail.publishedDate.toDate() ?? .now, + totalPages: bookDetail.pageCount + ) + + // 3. 표지정보 업데이트 + if let url = URL(string: bookDetail.coverURL) { + let data = try? Data(contentsOf: url) + book.coverImage = data + } + + // 4. 책정보 생성 + try? await bookRepository.createBook(book) + // 5. 책 생성에 실패하든 성공하든 새로운 책제목을 반환 + return book.name + } } } diff --git a/B.READ/B.READ/Sources/Domain/UseCaseImpl/QuoteUseCaseImpl.swift b/B.READ/B.READ/Sources/Domain/UseCaseImpl/QuoteUseCaseImpl.swift index a9e2f2d6..9ab1dfb2 100644 --- a/B.READ/B.READ/Sources/Domain/UseCaseImpl/QuoteUseCaseImpl.swift +++ b/B.READ/B.READ/Sources/Domain/UseCaseImpl/QuoteUseCaseImpl.swift @@ -12,6 +12,7 @@ final class QuoteUseCaseImpl: QuoteUseCase { private let userInfoRepository: UserInfoRepository private let quoteRepository: QuoteRepository private let bookRepository: BookRepository + private let bookService: BookService /// 생성자 /// - Parameters: @@ -20,11 +21,13 @@ final class QuoteUseCaseImpl: QuoteUseCase { init( userInfoRepository: UserInfoRepository, quoteRepository: QuoteRepository, - bookRepository: BookRepository + bookRepository: BookRepository, + bookService: BookService ) { self.userInfoRepository = userInfoRepository self.quoteRepository = quoteRepository self.bookRepository = bookRepository + self.bookService = bookService } func saveQuote(_ quote: Quote, in record: Record) async throws { @@ -53,10 +56,36 @@ final class QuoteUseCaseImpl: QuoteUseCase { return try await quoteRepository.fetchAllQuotes() } - - // TODO: - 조회한 도서가 없을 경우 알라딘 검색 후 도서 저장 -> 도서 제목 반환 func loadBookTitle(_ isbn: String) async throws -> String { - return try await bookRepository.fetchBook(isbn: isbn).name + do { + return try await bookRepository.fetchBook(isbn: isbn).name + } catch RepositoryError.dataNotFound { + // 도서 정보가 없다면 알라딘에서 검색 후 정보 생성하고 도서제목을 반환 + // 1. 알라딘에서 정보를 패치 + let bookDetail = try await bookService.fetchBookDetail(isbn: isbn) + + // 2. 패치한 정보로 엔티티 생성 + var book = Book( + isbn: bookDetail.isbn, + coverImage: nil, + name: bookDetail.title, + author: bookDetail.author, + publisher: bookDetail.publisher, + publishedAt: bookDetail.publishedDate.toDate() ?? .now, + totalPages: bookDetail.pageCount + ) + + // 3. 표지정보 업데이트 + if let url = URL(string: bookDetail.coverURL) { + let data = try? Data(contentsOf: url) + book.coverImage = data + } + + // 4. 책정보 생성 + try? await bookRepository.createBook(book) + // 5. 책 생성에 실패하든 성공하든 새로운 책제목을 반환 + return book.name + } } } diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/RecordPropertyRow.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/RecordPropertyRow.swift index b9338d93..4c7df546 100644 --- a/B.READ/B.READ/Sources/Presentation/Common/Components/RecordPropertyRow.swift +++ b/B.READ/B.READ/Sources/Presentation/Common/Components/RecordPropertyRow.swift @@ -27,55 +27,55 @@ struct RecordPropertyRow: View { } } -//// MARK: - Preview -//#Preview { -// let notStartdata = RecordCellVO( -// id: DummyData.dummyRecords[0].id, -// isbn: DummyData.dummyRecords[0].isbn, -// title: DummyData.dummyBooks[0].name, -// coverImage: Image(.exampleBook), -// readingState: ReadingState.fromEntity(DummyData.dummyRecords[0].state), -// heart: DummyData.dummyRecords[0].heartCount, -// progress: 0, -// star: DummyData.dummyRecords[0].starCount, -// memoCount: DummyData.dummyRecords[0].memos.count, -// quoteCount: DummyData.dummyRecords[0].quotes.count, -// period: DummyData.dummyRecords[0].period, -// isFavorite: DummyData.dummyRecords[0].isFavorite, -// createdAt: DummyData.dummyRecords[0].createdAt -// ) -// let readingdata = RecordCellVO( -// id: DummyData.dummyRecords[1].id, -// isbn: DummyData.dummyRecords[1].isbn, -// title: DummyData.dummyBooks[1].name, -// coverImage: Image(.exampleBook), -// readingState: ReadingState.fromEntity(DummyData.dummyRecords[1].state), -// heart: DummyData.dummyRecords[1].heartCount, -// progress: 65, -// star: DummyData.dummyRecords[1].starCount, -// memoCount: DummyData.dummyRecords[1].memos.count, -// quoteCount: DummyData.dummyRecords[1].quotes.count, -// period: DummyData.dummyRecords[1].period, -// isFavorite: DummyData.dummyRecords[1].isFavorite, -// createdAt: DummyData.dummyRecords[1].createdAt -// ) -// let finisheddata = RecordCellVO( -// id: DummyData.dummyRecords[2].id, -// isbn: DummyData.dummyRecords[2].isbn, -// title: DummyData.dummyBooks[2].name, -// coverImage: Image(.exampleBook), -// readingState: ReadingState.fromEntity(DummyData.dummyRecords[2].state), -// heart: DummyData.dummyRecords[2].heartCount, -// progress: 65, -// star: DummyData.dummyRecords[2].starCount, -// memoCount: DummyData.dummyRecords[2].memos.count, -// quoteCount: DummyData.dummyRecords[2].quotes.count, -// period: DummyData.dummyRecords[2].period, -// isFavorite: DummyData.dummyRecords[2].isFavorite, -// createdAt: DummyData.dummyRecords[2].createdAt -// ) -// -// RecordPropertyRow(data: notStartdata) -// RecordPropertyRow(data: readingdata) -// RecordPropertyRow(data: finisheddata) -//} +// MARK: - Preview +#Preview { + let notStartdata = RecordCellVO( + id: DummyData.dummyRecords[0].id, + isbn: DummyData.dummyRecords[0].isbn, + title: DummyData.dummyBooks[0].name, + coverImage: Image(.exampleCover), + readingState: ReadingState.fromEntity(DummyData.dummyRecords[0].state), + heart: DummyData.dummyRecords[0].heartCount, + progress: 0, + star: DummyData.dummyRecords[0].starCount, + memoCount: DummyData.dummyRecords[0].memos.count, + quoteCount: DummyData.dummyRecords[0].quotes.count, + period: DummyData.dummyRecords[0].period, + isFavorite: DummyData.dummyRecords[0].isFavorite, + createdAt: DummyData.dummyRecords[0].createdAt + ) + let readingdata = RecordCellVO( + id: DummyData.dummyRecords[1].id, + isbn: DummyData.dummyRecords[1].isbn, + title: DummyData.dummyBooks[1].name, + coverImage: Image(.exampleCover), + readingState: ReadingState.fromEntity(DummyData.dummyRecords[1].state), + heart: DummyData.dummyRecords[1].heartCount, + progress: 65, + star: DummyData.dummyRecords[1].starCount, + memoCount: DummyData.dummyRecords[1].memos.count, + quoteCount: DummyData.dummyRecords[1].quotes.count, + period: DummyData.dummyRecords[1].period, + isFavorite: DummyData.dummyRecords[1].isFavorite, + createdAt: DummyData.dummyRecords[1].createdAt + ) + let finisheddata = RecordCellVO( + id: DummyData.dummyRecords[2].id, + isbn: DummyData.dummyRecords[2].isbn, + title: DummyData.dummyBooks[2].name, + coverImage: Image(.exampleCover), + readingState: ReadingState.fromEntity(DummyData.dummyRecords[2].state), + heart: DummyData.dummyRecords[2].heartCount, + progress: 65, + star: DummyData.dummyRecords[2].starCount, + memoCount: DummyData.dummyRecords[2].memos.count, + quoteCount: DummyData.dummyRecords[2].quotes.count, + period: DummyData.dummyRecords[2].period, + isFavorite: DummyData.dummyRecords[2].isFavorite, + createdAt: DummyData.dummyRecords[2].createdAt + ) + + RecordPropertyRow(data: notStartdata) + RecordPropertyRow(data: readingdata) + RecordPropertyRow(data: finisheddata) +} diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/SortMenu.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/SortMenu.swift index 460fb2f5..9ebb0fe1 100644 --- a/B.READ/B.READ/Sources/Presentation/Common/Components/SortMenu.swift +++ b/B.READ/B.READ/Sources/Presentation/Common/Components/SortMenu.swift @@ -8,7 +8,6 @@ import Foundation import SwiftUI -// TODO: - [시르] 메뉴 등장 및 사라질때 애니메이션 추가하기 // MARK: - (S)SortMenuButton struct SortMenu: View { @Binding var isOpened: Bool diff --git a/B.READ/B.READ/Sources/Presentation/Common/ValueObject/RecordDetailVO.swift b/B.READ/B.READ/Sources/Presentation/Common/ValueObject/RecordDetailVO.swift index 84487884..974133ed 100644 --- a/B.READ/B.READ/Sources/Presentation/Common/ValueObject/RecordDetailVO.swift +++ b/B.READ/B.READ/Sources/Presentation/Common/ValueObject/RecordDetailVO.swift @@ -109,7 +109,6 @@ extension RecordDetailVO: Hashable { } extension RecordDetailVO { - // TODO: - [시르] 아이디어 필요! VO에 메모, 문장을 넣는방법도 있음 func toEntity(memos: [MemoVO] = [], quotes: [QuoteVO] = []) -> Record { Record( id: self.id, diff --git a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridView.swift b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridView.swift index 9b249cd5..b74c3f24 100644 --- a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridView.swift +++ b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridView.swift @@ -8,7 +8,6 @@ import SwiftUI // MARK: - (S)LibraryGridView -// TODO: - [시르] 그리드 뷰 구현 struct LibraryGridView: View { @EnvironmentObject var coordinator: Coordinator @Binding var records: [RecordCellVO] @@ -25,7 +24,6 @@ struct LibraryGridView: View { ForEach($records) { $record in LibraryGridCell(record: $record) .onTapGesture { - // TODO: - [시르] 머지 후, 뷰 연결 수정 coordinator.push(.libraryDetail(id: record.id)) } } diff --git a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListCell.swift b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListCell.swift index 43dfea62..c76b410a 100644 --- a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListCell.swift +++ b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListCell.swift @@ -27,9 +27,7 @@ struct LibraryListCell: View { .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1) // 독서 현황 - // TODO: - [시르] Binding으로 만들어야하면, 제작해서 사용 RecordPropertyRow(data: record) -// RecordStatsView(record: $record) // 독서 기간 periodView(record.period) diff --git a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/RecordStatsView.swift b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/RecordStatsView.swift deleted file mode 100644 index a1dd55fe..00000000 --- a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/RecordStatsView.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// RecordStatsView.swift -// B.READ -// -// Created by 심근웅 on 5/23/25. -// - -import SwiftUI - -// TODO: - Binding으로 안만들어도 뷰 반영되면 삭제 예정 -// MARK: - (S)RecordStatsView -struct RecordStatsView: View { - - @Binding private var record: RecordCellVO - - init(record: Binding) { - self._record = record - } - - var body: some View { - HStack(spacing: 12) { - switch record.readingState { - case .notStart: // 기대지수 - PropertyView(SFSymbol.heart.name, record.heart.toString) - case .reading: // 독서진행률 - PropertyView(SFSymbol.timer.name, record.progress.toString, .percent) - case .finished: // 평점 - PropertyView(SFSymbol.star.name, record.star.toString) - } - PropertyView(SFSymbol.memo.name, "\(record.memoCount)", .count) // 메모 - PropertyView(SFSymbol.bubble.name, "\(record.quoteCount)", .count) // 문장 - } // : HStack - .brStyleFont(.pretendard(.regular, size: 14), lineHeight: 1) - } -} - -#Preview { - @Previewable @State var record1 = RecordCellVO( - record: DummyData.dummyRecords[0], - book: DummyData.dummyBooks[0] - ) - @Previewable @State var record2 = RecordCellVO( - record: DummyData.dummyRecords[1], - book: DummyData.dummyBooks[1] - ) - @Previewable @State var record3 = RecordCellVO( - record: DummyData.dummyRecords[2], - book: DummyData.dummyBooks[2] - ) - - PreviewableContainer { - RecordStatsView(record: $record1) - RecordStatsView(record: $record2) - RecordStatsView(record: $record3) - } -} diff --git a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordBookSection.swift b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordBookSection.swift index 79913429..cf4e2d1b 100644 --- a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordBookSection.swift +++ b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordBookSection.swift @@ -19,7 +19,6 @@ struct RecordBookSection: View { .resizable() .aspectRatio(contentMode: .fill) } else { - // TODO: - [시르] 사진이 없을때, 들어갈 이미지 or 도형 추가 Image(.exampleCover) .resizable() .aspectRatio(contentMode: .fill) diff --git a/B.READ/B.READ/Sources/Presentation/Library/ViewModel/LibraryViewModel.swift b/B.READ/B.READ/Sources/Presentation/Library/ViewModel/LibraryViewModel.swift index a7400dfe..5d42f609 100644 --- a/B.READ/B.READ/Sources/Presentation/Library/ViewModel/LibraryViewModel.swift +++ b/B.READ/B.READ/Sources/Presentation/Library/ViewModel/LibraryViewModel.swift @@ -36,6 +36,7 @@ final class LibraryViewModel: ObservableObject { // DB에서 가져온 전체 독서기록 private var records: [RecordCellVO] = [] private var filteredRecords: [RecordCellVO] = [] + private var currentTask: Task? = nil // MARK: - Dependency @Dependency @@ -69,8 +70,13 @@ private extension LibraryViewModel { // 독서 기록을 불러옴 func loadRecords() { viewState = .loading - Task { + + currentTask?.cancel() + + currentTask = Task { + try? Task.checkCancellation() await fetchRecords() + await withTaskGroup(of: Void.self) { group in group.addTask { // 2. 불러온 독서 기록의 상태별 개수 확인 @@ -84,6 +90,7 @@ private extension LibraryViewModel { await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab]) } } + await MainActor.run { viewState = .loaded } @@ -92,17 +99,23 @@ private extension LibraryViewModel { // 상단 탭바를 선택 func selectTab() { - Task { + currentTask?.cancel() + + currentTask = Task { + try? Task.checkCancellation() // 1. 선택된 탭을 기준으로 필터 적용 await self.filterRecords() // 2. 필터 적용된 독서 기록에 정렬 적용 await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab]) } } - + // 정렬을 선택 func selectSort() { - Task { + currentTask?.cancel() + + currentTask = Task { + try? Task.checkCancellation() await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab]) } } @@ -119,7 +132,7 @@ private extension LibraryViewModel { // 2. Entity -> VO self.records = infos.map { RecordCellVO(record: $0.record, book: $0.book) } } catch { - // 3. 패치하던중 오류 발생 시 배열은 빈 배열을 반환하고, 에러 메시지를 띄움 + // 3. 패치하던중 오류 발생 시 배열은 빈 배열을 반환 print(error.localizedDescription) self.records = [] } diff --git a/B.READ/B.READ/Sources/Presentation/Library/ViewModel/RecordDetailViewModel.swift b/B.READ/B.READ/Sources/Presentation/Library/ViewModel/RecordDetailViewModel.swift index b9e60f22..ffddeef6 100644 --- a/B.READ/B.READ/Sources/Presentation/Library/ViewModel/RecordDetailViewModel.swift +++ b/B.READ/B.READ/Sources/Presentation/Library/ViewModel/RecordDetailViewModel.swift @@ -25,6 +25,7 @@ final class RecordDetailViewModel: ObservableObject { var selectedQuote: QuoteVO? = nil var selectedMemo: MemoVO? = nil var summary: SummaryVO? = nil + private var currentTask: Task? = nil init(recordID: String) { self.recordID = recordID @@ -74,10 +75,11 @@ private extension RecordDetailViewModel { /// 독서 기록 조회에서 필요한 정보를 불러옴 func loadInfo(id: String) { - Task { [weak self] in - guard let self = self else { return } + currentTask?.cancel() + + currentTask = Task { + try? Task.checkCancellation() - // TODO: - [시르] VO 생성은 전부 비동기 그룹처리 do { // 1. 독서 기록 정보를 불러옴 let info: (record: Record, book: Book) = try await libraryUseCase.loadRecord(id) @@ -118,9 +120,7 @@ private extension RecordDetailViewModel { /// 즐겨 찾기 정보를 업데이트 func toggleIsFavorite() { - Task { [weak self] in - guard let self = self else { return } - + Task { // 1. 즐겨 찾기 정보를 토글 await MainActor.run { self.record?.isFavorite.toggle() @@ -138,7 +138,6 @@ private extension RecordDetailViewModel { // 4. 생선한 Entity로 업데이트 try await libraryUseCase.editRecord(recordEntity) } catch { - // TODO: - [시르] 수정 실패 에러 메시지 추가 print(error.localizedDescription) } } @@ -146,9 +145,7 @@ private extension RecordDetailViewModel { /// 독서 기록을 삭제 func deleteRecord() { - Task { [weak self] in - guard let self = self else { return } - + Task { // 1. 현재 레코드 정보 확인 guard let record = self.record else { print("ViewModel Error: Record Not Found") @@ -160,8 +157,7 @@ private extension RecordDetailViewModel { let recordEntity = record.toEntity(memos: memos, quotes: quotes) // 3. 독서 기록을 삭제 try await libraryUseCase.deleteRecord(recordEntity) - } catch { - // TODO: - [시르] 삭제 실패에 따른 에러 처리 + } catch { print(error.localizedDescription) } } @@ -195,9 +191,7 @@ private extension RecordDetailViewModel { /// 문장 정렬 func sortQuotes() async { - Task { [weak self] in - guard let self = self else { return } - + Task { let by = self.selectedSort[1] // 1. 정렬한 결과 let sortQuotes: [QuoteVO] = quotes.sorted(by: by.sort) diff --git a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordMemoViewModel.swift b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordMemoViewModel.swift index c590e5d4..15ad41fb 100644 --- a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordMemoViewModel.swift +++ b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordMemoViewModel.swift @@ -19,6 +19,7 @@ final class RecordMemoViewModel: ObservableObject { // MARK: - Internal Variable private(set) var memoGroups: [MemoGroup] = [] var selectedMemo: MemoVO? = nil + private var currentTask: Task? = nil // MARK: - Dependency @Dependency private var memoUseCase: MemoUseCase @@ -52,7 +53,11 @@ final class RecordMemoViewModel: ObservableObject { private extension RecordMemoViewModel { /// 메모를 불러와서 뷰에 보여줄 형태로 가공합니다. func loadMemoGroups() { - Task { + currentTask?.cancel() + + currentTask = Task { + try? Task.checkCancellation() + do { // 1. 전체 독서 기록을 받아옴 let allRecords = try await libraryUseCase.loadRecordList() diff --git a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordQuoteViewModel.swift b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordQuoteViewModel.swift index ef54ce6f..fd583ce7 100644 --- a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordQuoteViewModel.swift +++ b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordQuoteViewModel.swift @@ -19,6 +19,7 @@ final class RecordQuoteViewModel: ObservableObject { // MARK: - Internal Variable private(set) var quoteGroups: [QuoteGroup] = [] var selectedQuote: QuoteVO? = nil + private var currentTask: Task? = nil // MARK: - Dependency @Dependency private var quoteUseCase: QuoteUseCase @@ -53,7 +54,11 @@ final class RecordQuoteViewModel: ObservableObject { private extension RecordQuoteViewModel { /// 문장을 불러와서 뷰에 보여줄 형태로 가공합니다. func loadQuoteGroups() { - Task { + currentTask?.cancel() + + currentTask = Task { + try? Task.checkCancellation() + do { // 1. 전체 독서 기록을 받아옴 let allRecords = try await libraryUseCase.loadRecordList() diff --git a/B.READ/UsecaseTest/LibraryUseCaseTest.swift b/B.READ/UsecaseTest/LibraryUseCaseTest.swift index efa2f3a9..3deaa428 100644 --- a/B.READ/UsecaseTest/LibraryUseCaseTest.swift +++ b/B.READ/UsecaseTest/LibraryUseCaseTest.swift @@ -17,7 +17,6 @@ struct LibraryUseCaseTest { private let userInfoRepository: UserInfoRepository private let recordRepository: RecordRepository private let bookRepository: BookRepository - private let quoteRepository: QuoteRepository private let bookService: BookService init() { @@ -26,14 +25,12 @@ struct LibraryUseCaseTest { userInfoRepository = UserInfoRepositoryImpl(modelContainer: storage.modelContainer) recordRepository = RecordRepositoryImpl(modelContainer: storage.modelContainer) bookRepository = BookRepositoryImpl(modelContainer: storage.modelContainer) - quoteRepository = QuoteRepositoryImpl(modelContainer: storage.modelContainer) bookService = AladinService(client: MockNetworkClient(nextMockFileName: "SearchList")) libraryUseCase = LibraryUseCaseImpl( userInfoRepository: userInfoRepository, bookRepository: bookRepository, recordRepository: recordRepository, - quoteRepository: quoteRepository, bookService: bookService ) }