diff --git a/B.READ/B.READ.xcodeproj/project.pbxproj b/B.READ/B.READ.xcodeproj/project.pbxproj
index acedcf11..61158b0b 100644
--- a/B.READ/B.READ.xcodeproj/project.pbxproj
+++ b/B.READ/B.READ.xcodeproj/project.pbxproj
@@ -60,7 +60,6 @@
Sources/Data/DTOs/Record/QuoteDTO.swift,
Sources/Data/DTOs/Record/RecordDTO.swift,
Sources/Data/DTOs/Record/SummaryDTO.swift,
- Sources/Data/DTOs/Record/TagDTO.swift,
Sources/Data/DTOs/UserInfo/CategoryDTO.swift,
Sources/Data/DTOs/UserInfo/DailyStatusDTO.swift,
Sources/Data/DTOs/UserInfo/KeywordDTO.swift,
@@ -98,7 +97,6 @@
Sources/Data/DTOs/Record/QuoteDTO.swift,
Sources/Data/DTOs/Record/RecordDTO.swift,
Sources/Data/DTOs/Record/SummaryDTO.swift,
- Sources/Data/DTOs/Record/TagDTO.swift,
Sources/Data/DTOs/UserInfo/CategoryDTO.swift,
Sources/Data/DTOs/UserInfo/DailyStatusDTO.swift,
Sources/Data/DTOs/UserInfo/KeywordDTO.swift,
@@ -152,7 +150,6 @@
Sources/Network/NetworkClient/NetworkClient.swift,
Sources/Network/NetworkClient/RequestConvertible.swift,
"Sources/Util/Extensions/Bundle+.swift",
- "Sources/Util/Extensions/Date+.swift",
);
target = 16BE931F2DED713E006FFD00 /* ServiceTest */;
};
@@ -167,7 +164,6 @@
Sources/Data/DTOs/Record/QuoteDTO.swift,
Sources/Data/DTOs/Record/RecordDTO.swift,
Sources/Data/DTOs/Record/SummaryDTO.swift,
- Sources/Data/DTOs/Record/TagDTO.swift,
Sources/Data/DTOs/UserInfo/CategoryDTO.swift,
Sources/Data/DTOs/UserInfo/DailyStatusDTO.swift,
Sources/Data/DTOs/UserInfo/KeywordDTO.swift,
@@ -221,7 +217,6 @@
Sources/Network/NetworkClient/NetworkClient.swift,
Sources/Network/NetworkClient/RequestConvertible.swift,
"Sources/Util/Extensions/Bundle+.swift",
- "Sources/Util/Extensions/Date+.swift",
);
target = 16BE95272DED72FB006FFD00 /* UsecaseTest */;
};
diff --git a/B.READ/B.READ/B.READ.entitlements b/B.READ/B.READ/B.READ.entitlements
new file mode 100644
index 00000000..014e51e9
--- /dev/null
+++ b/B.READ/B.READ/B.READ.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.BREAD
+
+
+
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/ExampleCover.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/ExampleBookImage.imageset/Contents.json
similarity index 86%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/ExampleCover.imageset/Contents.json
rename to B.READ/B.READ/Resources/Assets.xcassets/ExampleBookImage.imageset/Contents.json
index a8ac1d85..571e1220 100644
--- a/B.READ/B.READ/Resources/Assets.xcassets/Image/ExampleCover.imageset/Contents.json
+++ b/B.READ/B.READ/Resources/Assets.xcassets/ExampleBookImage.imageset/Contents.json
@@ -5,7 +5,7 @@
"scale" : "1x"
},
{
- "filename" : "ExampleCover.png",
+ "filename" : "ExampleBookImage.png",
"idiom" : "universal",
"scale" : "2x"
},
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/ExampleBookImage.imageset/ExampleBookImage.png b/B.READ/B.READ/Resources/Assets.xcassets/ExampleBookImage.imageset/ExampleBookImage.png
new file mode 100644
index 00000000..6a967ab9
Binary files /dev/null and b/B.READ/B.READ/Resources/Assets.xcassets/ExampleBookImage.imageset/ExampleBookImage.png differ
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/ExampleCover.imageset/ExampleCover.png b/B.READ/B.READ/Resources/Assets.xcassets/Image/ExampleCover.imageset/ExampleCover.png
deleted file mode 100644
index 84433a71..00000000
Binary files a/B.READ/B.READ/Resources/Assets.xcassets/Image/ExampleCover.imageset/ExampleCover.png and /dev/null differ
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/HappyBread.imageset/HappyBread.png b/B.READ/B.READ/Resources/Assets.xcassets/Image/HappyBread.imageset/HappyBread.png
index 672624f3..978666c9 100644
Binary files a/B.READ/B.READ/Resources/Assets.xcassets/Image/HappyBread.imageset/HappyBread.png and b/B.READ/B.READ/Resources/Assets.xcassets/Image/HappyBread.imageset/HappyBread.png differ
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/SadBread.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/Image/SadBread.imageset/Contents.json
deleted file mode 100644
index 6cf60e72..00000000
--- a/B.READ/B.READ/Resources/Assets.xcassets/Image/SadBread.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "filename" : "SadBread.png",
- "idiom" : "universal",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/SadBread.imageset/SadBread.png b/B.READ/B.READ/Resources/Assets.xcassets/Image/SadBread.imageset/SadBread.png
deleted file mode 100644
index b4b40ae8..00000000
Binary files a/B.READ/B.READ/Resources/Assets.xcassets/Image/SadBread.imageset/SadBread.png and /dev/null differ
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Bread3D1x.png "b/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/ChatGPT Image 2025\353\205\204 6\354\233\224 7\354\235\274 \354\230\244\355\233\204 02_20_53 1.png"
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Bread3D1x.png
rename to "B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/ChatGPT Image 2025\353\205\204 6\354\233\224 7\354\235\274 \354\230\244\355\233\204 02_20_53 1.png"
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Bread3D2x.png "b/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/ChatGPT Image 2025\353\205\204 6\354\233\224 7\354\235\274 \354\230\244\355\233\204 02_20_53 1@2x.png"
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Bread3D2x.png
rename to "B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/ChatGPT Image 2025\353\205\204 6\354\233\224 7\354\235\274 \354\230\244\355\233\204 02_20_53 1@2x.png"
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Bread3D3x.png "b/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/ChatGPT Image 2025\353\205\204 6\354\233\224 7\354\235\274 \354\230\244\355\233\204 02_20_53 1@3x.png"
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Bread3D3x.png
rename to "B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/ChatGPT Image 2025\353\205\204 6\354\233\224 7\354\235\274 \354\230\244\355\233\204 02_20_53 1@3x.png"
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Contents.json
index df104fc8..2a736de0 100644
--- a/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Contents.json
+++ b/B.READ/B.READ/Resources/Assets.xcassets/Image/SplashImage.imageset/Contents.json
@@ -1,17 +1,17 @@
{
"images" : [
{
- "filename" : "Bread3D1x.png",
+ "filename" : "ChatGPT Image 2025년 6월 7일 오후 02_20_53 1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "Bread3D2x.png",
+ "filename" : "ChatGPT Image 2025년 6월 7일 오후 02_20_53 1@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
- "filename" : "Bread3D3x.png",
+ "filename" : "ChatGPT Image 2025년 6월 7일 오후 02_20_53 1@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/TopLogo.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/Image/TopLogo.imageset/Contents.json
deleted file mode 100644
index 6a384168..00000000
--- a/B.READ/B.READ/Resources/Assets.xcassets/Image/TopLogo.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "filename" : "TopLogo.png",
- "idiom" : "universal",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/TopLogo.imageset/TopLogo.png b/B.READ/B.READ/Resources/Assets.xcassets/Image/TopLogo.imageset/TopLogo.png
deleted file mode 100644
index 0ea4aca7..00000000
Binary files a/B.READ/B.READ/Resources/Assets.xcassets/Image/TopLogo.imageset/TopLogo.png and /dev/null differ
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/ReadBreadMyPage.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/ReadBreadMyPage.imageset/Contents.json
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/ReadBreadMyPage.imageset/Contents.json
rename to B.READ/B.READ/Resources/Assets.xcassets/ReadBreadMyPage.imageset/Contents.json
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/ReadBreadMyPage.imageset/ReadBreadMyPage.png b/B.READ/B.READ/Resources/Assets.xcassets/ReadBreadMyPage.imageset/ReadBreadMyPage.png
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/ReadBreadMyPage.imageset/ReadBreadMyPage.png
rename to B.READ/B.READ/Resources/Assets.xcassets/ReadBreadMyPage.imageset/ReadBreadMyPage.png
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampF.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/StampF.imageset/Contents.json
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampF.imageset/Contents.json
rename to B.READ/B.READ/Resources/Assets.xcassets/StampF.imageset/Contents.json
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampF.imageset/StampF.png b/B.READ/B.READ/Resources/Assets.xcassets/StampF.imageset/StampF.png
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampF.imageset/StampF.png
rename to B.READ/B.READ/Resources/Assets.xcassets/StampF.imageset/StampF.png
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampM.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/StampM.imageset/Contents.json
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampM.imageset/Contents.json
rename to B.READ/B.READ/Resources/Assets.xcassets/StampM.imageset/Contents.json
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampM.imageset/StampM.png b/B.READ/B.READ/Resources/Assets.xcassets/StampM.imageset/StampM.png
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampM.imageset/StampM.png
rename to B.READ/B.READ/Resources/Assets.xcassets/StampM.imageset/StampM.png
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampS.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/StampS.imageset/Contents.json
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampS.imageset/Contents.json
rename to B.READ/B.READ/Resources/Assets.xcassets/StampS.imageset/Contents.json
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampS.imageset/StampS.png b/B.READ/B.READ/Resources/Assets.xcassets/StampS.imageset/StampS.png
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampS.imageset/StampS.png
rename to B.READ/B.READ/Resources/Assets.xcassets/StampS.imageset/StampS.png
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampT.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/StampT.imageset/Contents.json
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampT.imageset/Contents.json
rename to B.READ/B.READ/Resources/Assets.xcassets/StampT.imageset/Contents.json
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampT.imageset/StampT.png b/B.READ/B.READ/Resources/Assets.xcassets/StampT.imageset/StampT.png
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampT.imageset/StampT.png
rename to B.READ/B.READ/Resources/Assets.xcassets/StampT.imageset/StampT.png
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampW.imageset/Contents.json b/B.READ/B.READ/Resources/Assets.xcassets/StampW.imageset/Contents.json
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampW.imageset/Contents.json
rename to B.READ/B.READ/Resources/Assets.xcassets/StampW.imageset/Contents.json
diff --git a/B.READ/B.READ/Resources/Assets.xcassets/Image/StampW.imageset/StampW.png b/B.READ/B.READ/Resources/Assets.xcassets/StampW.imageset/StampW.png
similarity index 100%
rename from B.READ/B.READ/Resources/Assets.xcassets/Image/StampW.imageset/StampW.png
rename to B.READ/B.READ/Resources/Assets.xcassets/StampW.imageset/StampW.png
diff --git a/B.READ/B.READ/Sources/App/Coordinator/Coordinator.swift b/B.READ/B.READ/Sources/App/Coordinator/Coordinator.swift
index 0853b292..0685c05a 100644
--- a/B.READ/B.READ/Sources/App/Coordinator/Coordinator.swift
+++ b/B.READ/B.READ/Sources/App/Coordinator/Coordinator.swift
@@ -23,22 +23,22 @@ final class Coordinator: ObservableObject {
// MARK: - Navigation Push/Pop
func push(_ path: T) {
guard paths.last != path else { return }
- print("Before push:\n\(paths.map { "\($0)\n" }.joined(separator: "\n"))\n")
+ print("Before push: \(paths)")
paths.append(path)
- print("After push:\n\(paths.map { "\($0)\n" }.joined(separator: "\n"))\n")
+ print("After push: \(paths)")
}
func pop() {
guard !paths.isEmpty else { return }
- print("Before pop:\n\(paths.map { "\($0)\n" }.joined(separator: "\n"))\n")
+ print("Before pop: \(paths)")
paths.removeLast()
- print("After pop:\n\(paths.map { "\($0)\n" }.joined(separator: "\n"))\n")
+ print("After pop: \(paths)")
}
func popToRoot() {
- print("Before popToRoot:\n\(paths.map { "\($0)\n" }.joined(separator: "\n"))\n")
+ print("Before popToRoot: \(paths)")
paths.removeAll()
- print("After popToRoot:\n\(paths.map { "\($0)\n" }.joined(separator: "\n"))\n")
+ print("After popToRoot: \(paths)")
}
func pop(to: T) {
diff --git a/B.READ/B.READ/Sources/App/Coordinator/MainCoordinator.swift b/B.READ/B.READ/Sources/App/Coordinator/MainCoordinator.swift
index ef4ecade..08ea08da 100644
--- a/B.READ/B.READ/Sources/App/Coordinator/MainCoordinator.swift
+++ b/B.READ/B.READ/Sources/App/Coordinator/MainCoordinator.swift
@@ -16,15 +16,13 @@ enum MainRoute: Hashable {
// MARK: - Library
case libraryDetail(id: String)
- case createSummary(record: RecordDetailVO, memos: [MemoVO], quotes: [QuoteVO])
- case summaryDetail(id: String, record: RecordDetailVO, memos: [MemoVO], quotes: [QuoteVO])
// MARK: - Sentence
case sentenceInput(mode: SentenceInputMode)
- case pageInput(record: RecordDetailVO, quote: QuoteVO)
+ case pageInput(mode: SentenceInputMode, sentence: String)
// MARK: - Memo
- // case memo(id: String? = nil, record: Record, totalPage: Int)
+// case memo(id: String? = nil, record: Record, totalPage: Int)
case memo(id: String? = nil, record: RecordDetailVO)
// MARK: - MyPage
@@ -35,8 +33,7 @@ enum MainRoute: Hashable {
enum SheetRoute: Identifiable {
case createRecord(
state: Binding,
- book: Book,
- onComplete: (_ isEdit: Bool) -> Void
+ book: Book
)
case updateRecord(
@@ -53,20 +50,17 @@ enum SheetRoute: Identifiable {
extension Coordinator where R == SheetRoute {
@ViewBuilder
func buildView(for route: R) -> some View {
- let factory = ViewModelFactory.shared
-
switch route {
- case let .createRecord(state, book, onComplete):
+ case let .createRecord(state, book):
CreateRecordView(
state: state,
- viewModel: factory.createNewRecord(book: book),
- onComplete: onComplete
+ viewModel: NewRecordViewModel(book: book)
)
case let .updateRecord(state, record, onComplete):
CreateRecordView(
state: state,
- viewModel: factory.createUpdateRecord(record: record),
+ viewModel: NewRecordViewModel(recordVO: record),
onComplete: onComplete
)
}
@@ -77,15 +71,13 @@ extension Coordinator where T == MainRoute {
@ViewBuilder
func buildView(for route: T) -> some View {
- let factory = ViewModelFactory.shared
-
switch route {
// MARK: - Search Flow
case .barcode:
- ScanView(viewModel: factory.createScan())
+ ScanView(viewModel: ScanViewModel())
case .searchBook(let isbn):
- BookDetailView(viewModel: factory.createBook(isbn: isbn))
+ BookDetailView(viewModel: BookViewModel(isbn: isbn))
case .goToWebView(let url):
WebView(url: url)
.navigationBarBackButtonHidden()
@@ -102,37 +94,18 @@ extension Coordinator where T == MainRoute {
// MARK: - Library
case .libraryDetail(let id):
- RecordDetailView(viewModel: factory.createRecordDetail(id: id))
- case let .createSummary(record, memos, quotes):
- AlanSummaryView(
- viewModel: factory.createSummary(
- record: record,
- memos: memos,
- quotes: quotes
- )
- )
- case let .summaryDetail(id, record, memos, quotes):
- AlanSummaryView(
- viewModel: factory.createSummaryDetail(
- id: id,
- record: record,
- memos: memos,
- quotes: quotes
- )
- )
+ RecordDetailView(viewModel: .init(recordID: id))
// MARK: - Sentence
case .sentenceInput(let mode):
- SentenceInputView(viewModel: factory.createSentenceInput(mode: mode))
- case .pageInput(let record, let quote):
- PageInputView(viewModel: factory.createPageInput(record: record, quote: quote))
-
+ SentenceInputView(mode: mode)
+
+ case .pageInput(let mode, let sentence):
+ PageInputView(mode: mode, sentence: sentence)
+
// MARK: - Memo
case .memo(let id, let record):
- MemoView(
- viewModel: factory.createMemo(id: id, record: record),
- totalPage: record.totalPage
- )
+ MemoView(viewModel: MemoViewModel(id: id, record: record), totalPage: record.totalPage)
// MARK: - MyPage Flow
case .insertNickname:
diff --git a/B.READ/B.READ/Sources/App/DIContainer/DIContainer.swift b/B.READ/B.READ/Sources/App/DIContainer/DIContainer.swift
index 80c1f12c..8cf4914b 100644
--- a/B.READ/B.READ/Sources/App/DIContainer/DIContainer.swift
+++ b/B.READ/B.READ/Sources/App/DIContainer/DIContainer.swift
@@ -49,7 +49,6 @@ extension DIContainer {
let recordRepository = RecordRepositoryImpl(modelContainer: storage.modelContainer)
let memoRepository = MemoRepositoryImpl(modelContainer: storage.modelContainer)
let quoteRepository = QuoteRepositoryImpl(modelContainer: storage.modelContainer)
- let summaryRepository = SummaryRepositoryImpl(modelContainer: storage.modelContainer)
// MARK: - UseCase 생성 및 등록
// Profile UseCase
@@ -57,11 +56,9 @@ extension DIContainer {
ProfileUseCaseImpl(userInfoRepository: userInfoRepository),
for: ProfileUseCase.self
)
-
// Library UseCase
self.shared.register(
LibraryUseCaseImpl(
- userInfoRepository: userInfoRepository,
bookRepository: bookRepository,
recordRepository: recordRepository,
quoteRepository: quoteRepository,
@@ -69,11 +66,10 @@ extension DIContainer {
),
for: LibraryUseCase.self
)
-
+
// Memo UseCase
self.shared.register(
MemoUseCaseImpl(
- userInfoRepository: userInfoRepository,
bookRepository: bookRepository,
memoRepository: memoRepository,
aiService: AlanService()
@@ -84,23 +80,11 @@ extension DIContainer {
// Quote UseCase
self.shared.register(
QuoteUseCaseImpl(
- userInfoRepository: userInfoRepository,
quoteRepository: quoteRepository,
bookRepository: bookRepository),
for: QuoteUseCase.self
)
-
- // Summary UseCase
- self.shared.register(
- SummaryUseCaseImpl(
- userInfoRepository: userInfoRepository,
- summaryRepository: summaryRepository,
- bookRepository: bookRepository,
- recordRepository: recordRepository,
- aiService: AlanService()
- ),
- for: SummaryUseCase.self
- )
+ // TODO: - Note UseCase
// Search UseCase
self.shared.register(
diff --git a/B.READ/B.READ/Sources/App/DIContainer/ViewModelFactory.swift b/B.READ/B.READ/Sources/App/DIContainer/ViewModelFactory.swift
deleted file mode 100644
index 1e1e7b48..00000000
--- a/B.READ/B.READ/Sources/App/DIContainer/ViewModelFactory.swift
+++ /dev/null
@@ -1,58 +0,0 @@
-//
-// ViewModelFactory.swift
-// B.READ
-//
-// Created by 김도연 on 6/11/25.
-//
-
-import Foundation
-
-final class ViewModelFactory {
- static let shared = ViewModelFactory()
- private init() {}
-
- // MARK: - Search
- func createScan() -> ScanViewModel {
- ScanViewModel()
- }
-
- func createBook(isbn: String) -> BookViewModel {
- BookViewModel(isbn: isbn)
- }
-
- // MARK: - Library
- func createRecordDetail(id: String) -> RecordDetailViewModel {
- RecordDetailViewModel(recordID: id)
- }
-
- func createSummary(record: RecordDetailVO, memos: [MemoVO], quotes: [QuoteVO]) -> SummaryViewModel {
- SummaryViewModel(record: record, memos: memos, quotes: quotes)
- }
-
- func createSummaryDetail(id: String, record: RecordDetailVO, memos: [MemoVO], quotes: [QuoteVO]) -> SummaryViewModel {
- SummaryViewModel(id: id, record: record, memos: memos, quotes: quotes)
- }
-
- // MARK: - Sentence
- func createSentenceInput(mode: SentenceInputMode) -> SentenceInputViewModel {
- SentenceInputViewModel(mode: mode)
- }
-
- func createPageInput(record: RecordDetailVO, quote: QuoteVO) -> PageInputViewModel {
- PageInputViewModel(record: record, quote: quote)
- }
-
- // MARK: - Memo
- func createMemo(id: String?, record: RecordDetailVO) -> MemoViewModel {
- MemoViewModel(id: id, record: record)
- }
-
- // MARK: - NewRecord
- func createNewRecord(book: Book) -> NewRecordViewModel {
- NewRecordViewModel(book: book)
- }
-
- func createUpdateRecord(record: RecordDetailVO) -> NewRecordViewModel {
- NewRecordViewModel(recordVO: record)
- }
-}
diff --git a/B.READ/B.READ/Sources/App/RootViewSwitcher.swift b/B.READ/B.READ/Sources/App/RootViewSwitcher.swift
index 5972e9ad..52980bac 100644
--- a/B.READ/B.READ/Sources/App/RootViewSwitcher.swift
+++ b/B.READ/B.READ/Sources/App/RootViewSwitcher.swift
@@ -31,12 +31,11 @@ struct RootViewSwitcher: View {
Group {
switch rootScene {
case .launch:
- LaunchScreen()
+ Color.white.ignoresSafeArea()
.task {
await DIContainer.config()
// TODO: - [더미]초기값 적용이라 마지막에 제거하기
// await DummyService.shared.setDummy()
- try? await Task.sleep(for: .seconds(2))
await MainActor.run { self.isReady = true }
}
case .onboarding:
diff --git a/B.READ/B.READ/Sources/Data/DTOs/Record/RecordDTO.swift b/B.READ/B.READ/Sources/Data/DTOs/Record/RecordDTO.swift
index 76e8bf49..bf807505 100644
--- a/B.READ/B.READ/Sources/Data/DTOs/Record/RecordDTO.swift
+++ b/B.READ/B.READ/Sources/Data/DTOs/Record/RecordDTO.swift
@@ -84,9 +84,7 @@ final class RecordDTO {
updatedAt: data.updatedAt
)
- if let summaryData = data.summary {
- self.summary = SummaryDTO(summaryData, record: self)
- }
+ self.summary = data.summary.map { SummaryDTO($0, record: self) }
self.memos = data.memos.map{ MemoDTO($0, record: self) }
self.quotes = data.quotes.map { QuoteDTO($0, record: self) }
}
diff --git a/B.READ/B.READ/Sources/Data/DTOs/Record/SummaryDTO.swift b/B.READ/B.READ/Sources/Data/DTOs/Record/SummaryDTO.swift
index 43e13888..a971ab1d 100644
--- a/B.READ/B.READ/Sources/Data/DTOs/Record/SummaryDTO.swift
+++ b/B.READ/B.READ/Sources/Data/DTOs/Record/SummaryDTO.swift
@@ -14,26 +14,14 @@ final class SummaryDTO {
var id: String
var isbn: String
var content: String
-
- @Relationship(deleteRule: .cascade)
- var tags: [TagDTO]
-
var createdAt: Date
- var record: RecordDTO?
+ var record: RecordDTO
- init(
- id: String,
- isbn: String,
- content: String,
- tags: [TagDTO],
- createdAt: Date,
- record: RecordDTO
- ) {
+ init(id: String, isbn: String, content: String, createdAt: Date, record: RecordDTO) {
self.id = id
self.isbn = isbn
self.content = content
- self.tags = tags
self.createdAt = createdAt
self.record = record
}
@@ -44,11 +32,7 @@ final class SummaryDTO {
self.content = data.content
self.createdAt = data.createdAt
self.record = record
- self.tags = []
-
- self.tags = data.tags.map { TagDTO($0) }
}
-
}
extension SummaryDTO {
@@ -57,7 +41,6 @@ extension SummaryDTO {
id: self.id,
isbn: self.isbn,
content: self.content,
- tags: self.tags.map{ $0.toEntity() },
createdAt: self.createdAt
)
}
diff --git a/B.READ/B.READ/Sources/Data/DTOs/Record/TagDTO.swift b/B.READ/B.READ/Sources/Data/DTOs/Record/TagDTO.swift
deleted file mode 100644
index 09b36152..00000000
--- a/B.READ/B.READ/Sources/Data/DTOs/Record/TagDTO.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-//
-// TagDTO.swift
-// B.READ
-//
-// Created by 김도연 on 6/9/25.
-//
-
-import Foundation
-import SwiftData
-
-@Model
-final class TagDTO {
- @Attribute(.unique)
- var id: String
- var content: String
-
- init(id: String, content: String) {
- self.id = id
- self.content = content
- }
-
- convenience init(_ data: Tag) {
- self.init(
- id: data.id,
- content: data.content
- )
- }
-}
-
-extension TagDTO {
- func toEntity() -> Tag {
- return Tag(
- id: self.id,
- content: self.content
- )
- }
-}
diff --git a/B.READ/B.READ/Sources/Data/Impls/RecordRepositoryImpl.swift b/B.READ/B.READ/Sources/Data/Impls/RecordRepositoryImpl.swift
index b56618c4..814cec7e 100644
--- a/B.READ/B.READ/Sources/Data/Impls/RecordRepositoryImpl.swift
+++ b/B.READ/B.READ/Sources/Data/Impls/RecordRepositoryImpl.swift
@@ -64,22 +64,6 @@ actor RecordRepositoryImpl: RecordRepository {
}
}
- func fetchRecordAvailableForSummary() throws -> Record {
- print("Impl: ", #function)
-
- let predicate = #Predicate { $0.state == 2 && $0.summary == nil && !$0.memos.isEmpty }
- let sort = SortDescriptor(\RecordDTO.updatedAt, order: .reverse)
- var descriptor = FetchDescriptor(predicate: predicate, sortBy: [sort])
- descriptor.fetchLimit = 1
-
- guard let data = try? modelContext.fetch(descriptor).first else {
- throw RepositoryError.dataNotFound
- }
-
- return data.toEntity()
- }
-
-
func deleteRecord(_ id: String) throws {
print("Impl: ", #function)
diff --git a/B.READ/B.READ/Sources/Data/Impls/SummaryRepositoryImpl.swift b/B.READ/B.READ/Sources/Data/Impls/SummaryRepositoryImpl.swift
deleted file mode 100644
index 51b71468..00000000
--- a/B.READ/B.READ/Sources/Data/Impls/SummaryRepositoryImpl.swift
+++ /dev/null
@@ -1,63 +0,0 @@
-//
-// SummaryRepositoryImpl.swift
-// B.READ
-//
-// Created by 김도연 on 6/9/25.
-//
-
-import Foundation
-import SwiftData
-
-@ModelActor
-actor SummaryRepositoryImpl: SummaryRepository {
-
- func createSummary(_ summary: AlanSummary, in record: Record) async throws {
- print("Impl: ", #function)
-
- if let _ = try findSummary(id: summary.id) {
- throw RepositoryError.dataAlreadyExist
- }
-
- let model = SummaryDTO(summary, record: RecordDTO(record))
- modelContext.insert(model)
-
- try modelContext.save()
- }
-
- func fetchSummary(id: String) async throws -> AlanSummary {
- print("Impl: ", #function)
-
- guard let data = try findSummary(id: id) else {
- throw RepositoryError.dataNotFound
- }
-
- return data.toEntity()
- }
-
- func fetchAllSummary() async throws -> [AlanSummary] {
- print("Impl: ", #function)
-
- let descriptor = FetchDescriptor()
-
- do {
- let data = try modelContext.fetch(descriptor)
- return data.map { $0.toEntity() }
- } catch {
- throw RepositoryError.fetchError
- }
- }
-
-}
-
-private extension SummaryRepositoryImpl {
- func findSummary(id: String) throws -> SummaryDTO? {
- let predicate = #Predicate { $0.id == id }
- let descriptor = FetchDescriptor(predicate: predicate)
-
- do {
- return try modelContext.fetch(descriptor).first
- } catch {
- throw RepositoryError.fetchError
- }
- }
-}
diff --git a/B.READ/B.READ/Sources/DesignSystem/Font/FontStyleModifier.swift b/B.READ/B.READ/Sources/DesignSystem/Font/FontStyleModifier.swift
index 0c7e0943..b6de65a2 100644
--- a/B.READ/B.READ/Sources/DesignSystem/Font/FontStyleModifier.swift
+++ b/B.READ/B.READ/Sources/DesignSystem/Font/FontStyleModifier.swift
@@ -24,22 +24,6 @@ struct FontStyleModifier: ViewModifier {
}
}
-struct StyleModifier: ViewModifier {
- let font: UIFont
- let lineHeight: CGFloat
- let letterSpacing: CGFloat
-
- func body(content: Content) -> some View {
-
- let lineSpacing = font.pointSize * (lineHeight - 1)
-
- return content
- .padding(.vertical, lineSpacing / 2)
- .lineSpacing(lineSpacing)
- .tracking(font.pointSize * letterSpacing)
- }
-}
-
extension View {
/// BR 스타일 폰트 적용 (줄간격 및 자간 설정)
/// - Parameters:
@@ -57,22 +41,4 @@ extension View {
letterSpacing: letterSpacing
))
}
-
- /// BR 스타일 적용 (줄간격 및 자간 설정)
- /// - Parameters:
- /// - font: 사용할 UIFont
- /// - lineHeight: 줄간격 배수 (예: 1.4 → 140%)
- /// - letterSpacing: 자간 배수 (예: 0.02 → 2%)
- /// - Note: BR 스타일 폰트에서 폰트 적용만 제외
- func brStyle(
- _ font: UIFont,
- lineHeight: CGFloat,
- letterSpacing: CGFloat = 0.0
- ) -> some View {
- self.modifier(StyleModifier(
- font: font,
- lineHeight: lineHeight,
- letterSpacing: letterSpacing)
- )
- }
}
diff --git a/B.READ/B.READ/Sources/Domain/Dummy/DummyData.swift b/B.READ/B.READ/Sources/Domain/Dummy/DummyData.swift
index 6c8ecfd5..eb91edd1 100644
--- a/B.READ/B.READ/Sources/Domain/Dummy/DummyData.swift
+++ b/B.READ/B.READ/Sources/Domain/Dummy/DummyData.swift
@@ -143,38 +143,11 @@ extension DummyData {
),
currentPage: 252,
review: "",
- memos: [
- Memo(
- id: "1",
- isbn: "9788937460586",
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 4))!,
- content: "싯다르타는 수많은 스승과 가르침을 거쳤지만, 결국 삶을 살아가는 과정에서 스스로 진리를 깨닫는다. 남의 말이 아닌, 체험이 지혜로 이어진다.",
- pages: (100, 132),
- guides: [Guide(date: fixedDate, content: "exmaple1"), Guide(date: fixedDate, content: "exmaple1")]
- ),
- Memo(
- id: "2",
- isbn: "9788937460586",
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 4))!,
- content: "강을 바라보며 싯다르타는 모든 존재가 연결되어 흐르고 있다는 사실을 깨닫는다. 나 또한 변화와 흐름을 있는 그대로 받아들이고 싶어졌다.",
- pages: (100, 132),
- guides: [Guide(date: fixedDate, content: "exmaple1"), Guide(date: fixedDate, content: "exmaple1")]
- ),
- Memo(
- id: "3",
- isbn: "9788937460586",
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 4))!,
- content: "싯다르타의 삶은 고통과 실수의 연속이었지만, 그 모든 것이 깨달음으로 나아가는 길이었다. 나 역시 내 방황을 긍정하고 싶어졌다.",
- pages: (100, 132),
- guides: [Guide(date: fixedDate, content: "exmaple1"), Guide(date: fixedDate, content: "exmaple1")]
- ),
- ],
+ memos: [],
quotes: [],
createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 4, day: 19))!,
updatedAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 10))!
)
-
-
// Record( // 아주 작은 습관들
// id: UUID().uuidString,
// isbn: "9791162540640",
@@ -362,102 +335,8 @@ extension DummyData {
Quote(id: "5", isbn: "9788937460586", content: "수집된 문장 문장 수집된 문장 수집된 문장 수집된 문장", page: 72),
]
}
-
-// MARK: - AI Note
-extension DummyData {
- static let bookForSummary = Book(
- isbn: "9788937460586",
- name: "싯다르타",
- author: "헤르만헤세",
- publisher: "민음사",
- publishedAt: Calendar.current.date(from: DateComponents(year: 2002, month: 1, day: 20))!,
- totalPages: 252
- )
-
- static let recordForSummary = Record( // 싯다르타
- id: "3",
- isbn: "9788937460586",
- state: .completed,
- heartCount: 0,
- starCount: 4,
- isFavorite: false,
- period: (
- Calendar.current.date(from: DateComponents(year: 2025, month: 4, day: 20)),
- Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 10))
- ),
- currentPage: 252,
- review: "",
- memos: [
- Memo(
- id: "1",
- isbn: "9788937460586",
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 4))!,
- content: "싯다르타는 수많은 스승과 가르침을 거쳤지만, 결국 삶을 살아가는 과정에서 스스로 진리를 깨닫는다. 남의 말이 아닌, 체험이 지혜로 이어진다.",
- pages: (100, 132),
- guides: [Guide(date: fixedDate, content: "exmaple1"), Guide(date: fixedDate, content: "exmaple1")]
- ),
- Memo(
- id: "2",
- isbn: "9788937460586",
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 4))!,
- content: "강을 바라보며 싯다르타는 모든 존재가 연결되어 흐르고 있다는 사실을 깨닫는다. 나 또한 변화와 흐름을 있는 그대로 받아들이고 싶어졌다.",
- pages: (100, 132),
- guides: [Guide(date: fixedDate, content: "exmaple1"), Guide(date: fixedDate, content: "exmaple1")]
- ),
- Memo(
- id: "3",
- isbn: "9788937460586",
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 4))!,
- content: "싯다르타의 삶은 고통과 실수의 연속이었지만, 그 모든 것이 깨달음으로 나아가는 길이었다. 나 역시 내 방황을 긍정하고 싶어졌다.",
- pages: (100, 132),
- guides: [Guide(date: fixedDate, content: "exmaple1"), Guide(date: fixedDate, content: "exmaple1")]
- ),
- ],
- quotes: [],
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 4, day: 19))!,
- updatedAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 10))!
- )
-
- static let summaryForFetchTest = AlanSummary(
- id: "summary-siddhartha",
- isbn: "9788937460586",
- content: "헤르만 헤세의 '싯다르타'는 주인공이 수많은 스승과 가르침을 거쳐가며, 결국 자신의 삶을 통해 진리를 깨닫는 과정을 그립니다. 싯다르타는 남의 말이 아닌 체험을 통해 지혜를 얻으며, 강을 바라보며 모든 존재가 연결되어 흐른다는 사실을 깨닫습니다. 이를 통해 나 또한 변화와 흐름을 있는 그대로 받아들이고 싶어졌습니다. 싯다르타의 삶은 고통과 실수의 연속이었지만, 그 모든 것이 깨달음으로 나아가는 길이었음을 보여줍니다. 나 역시 내 방황을 긍정하고 싶어졌습니다.",
- tags: [
- Tag(id: "f1", content: "깨달음"),
- Tag(id: "f2", content: "연결"),
- Tag(id: "f3", content: "변화"),
- Tag(id: "f4", content: "고통"),
- Tag(id: "f5", content: "긍정")
- ],
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 11))!
- )
-
- static let summary1 = AlanSummary(
- id: "summary-1",
- isbn: "9791194368137",
- content: "워런 버핏의 투자 철학 요약입니다.",
- tags: [Tag(id: "t1", content: "투자"), Tag(id: "t2", content: "버핏")],
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 18))!
- )
-
- static let summary2 = AlanSummary(
- id: "summary-2",
- isbn: "9791158510619",
- content: "타이탄의 도구들 핵심 내용을 정리했습니다.",
- tags: [Tag(id: "t3", content: "자기계발"), Tag(id: "t4", content: "성공")],
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 12))!
- )
-
- static let summary3 = AlanSummary(
- id: "summary-3",
- isbn: "9788937460586",
- content: "싯다르타의 삶과 깨달음 요약본.",
- tags: [Tag(id: "t5", content: "철학"), Tag(id: "t6", content: "삶")],
- createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 11))!
- )
-
- static var dummySummaries: [AlanSummary] {
- [summary1, summary2, summary3]
- }
-}
-
+//
+//// MARK: - AI Note
+//extension DummyData {
+//
+//}
diff --git a/B.READ/B.READ/Sources/Domain/Dummy/DummyService.swift b/B.READ/B.READ/Sources/Domain/Dummy/DummyService.swift
index 27e99fe0..69e92486 100644
--- a/B.READ/B.READ/Sources/Domain/Dummy/DummyService.swift
+++ b/B.READ/B.READ/Sources/Domain/Dummy/DummyService.swift
@@ -30,7 +30,7 @@ final class DummyService {
for quote in DummyData.dummyQuote {
for record in DummyData.dummyRecords {
if record.isbn == quote.isbn {
- try? await quoteUseCase.saveQuote(quote, in: record)
+ try? await quoteUseCase.addQuote(quote, in: record)
break
}
}
diff --git a/B.READ/B.READ/Sources/Domain/Entity/AlanSummary.swift b/B.READ/B.READ/Sources/Domain/Entity/AlanSummary.swift
index 05ee86cc..53ea364a 100644
--- a/B.READ/B.READ/Sources/Domain/Entity/AlanSummary.swift
+++ b/B.READ/B.READ/Sources/Domain/Entity/AlanSummary.swift
@@ -17,38 +17,12 @@ struct AlanSummary {
public let id: String
let isbn: String
let content: String
- let tags: [Tag]
let createdAt: Date
- init(id: String, isbn: String, content: String, tags: [Tag], createdAt: Date) {
+ init(id: String, isbn: String, content: String, createdAt: Date) {
self.id = id
self.isbn = isbn
self.content = content
- self.tags = tags
self.createdAt = createdAt
}
-
- init(isbn: String, content: String, tags: [Tag]) {
- self.init(
- id: UUID().uuidString,
- isbn: isbn,
- content: content,
- tags: tags,
- createdAt: .now
- )
- }
-}
-
-struct Tag {
- public let id: String
- let content: String
-
- init(id: String, content: String) {
- self.id = id
- self.content = content
- }
-
- init(_ content: String) {
- self.init(id: UUID().uuidString, content: content)
- }
}
diff --git a/B.READ/B.READ/Sources/Domain/Repository/Inerface/RecordRepository.swift b/B.READ/B.READ/Sources/Domain/Repository/Inerface/RecordRepository.swift
index bebffcbf..8bc735b5 100644
--- a/B.READ/B.READ/Sources/Domain/Repository/Inerface/RecordRepository.swift
+++ b/B.READ/B.READ/Sources/Domain/Repository/Inerface/RecordRepository.swift
@@ -41,13 +41,6 @@ protocol RecordRepository {
/// - `RepositoryError.fetchError`: 데이터 조회 중 에러가 발생한 경우
func fetchRecentReadingRecord(maxCount: Int) async throws -> [Record]
- /// 요약 생성을 위해 사용 가능한 `완독` 상태의 독서 기록을 최신순으로 조회합니다.
- ///
- /// - Returns: 요약이 아직 작성되지 않은 가장 최근의 `완독` 상태의 독서 기록 하나
- /// - Throws:
- /// - `RepositoryError.dataNotFound`: 해당 조건에 맞는 독서 기록이 존재하지 않는 경우
- func fetchRecordAvailableForSummary() async throws -> Record
-
/// 특정 Record를 갱신합니다.
///
/// - Parameter record: Record Entity
diff --git a/B.READ/B.READ/Sources/Domain/Repository/Inerface/SummaryRepository.swift b/B.READ/B.READ/Sources/Domain/Repository/Inerface/SummaryRepository.swift
deleted file mode 100644
index 90e37c16..00000000
--- a/B.READ/B.READ/Sources/Domain/Repository/Inerface/SummaryRepository.swift
+++ /dev/null
@@ -1,40 +0,0 @@
-//
-// SummaryRepository.swift
-// B.READ
-//
-// Created by 김도연 on 6/9/25.
-//
-
-import Foundation
-import SwiftData
-
-/// Summary 관련 데이터 접근을 담당하는 저장소 인터페이스입니다.
-protocol SummaryRepository {
-
- /// Summary를 생성합니다.
- ///
- /// - Parameters:
- /// - summary: 생성할 Summary Entity
- /// - record: summary가 속할 독서 기록
- /// - Throws:
- /// - `RepositoryError.dataAlreadyExist`: 동일한 ID의 Summary가 이미 존재하는 경우
- /// - `RepositoryError.fetchError`: 저장 중 에러가 발생한 경우
- func createSummary(_ summary: AlanSummary, in record: Record) async throws
-
- /// 특정 ID에 해당하는 Summary를 조회합니다.
- ///
- /// - Parameter id: 조회할 Summary의 ID
- /// - Returns: Summary Entity
- /// - Throws:
- /// - `RepositoryError.dataNotFound`: 해당 Summary가 존재하지 않는 경우
- /// - `RepositoryError.fetchError`: 조회 중 에러가 발생한 경우
- func fetchSummary(id: String) async throws -> AlanSummary
-
- /// 전체 Summary를 조회합니다.
- ///
- /// - Returns: Summary Entity 리스트
- /// - Throws:
- /// - `RepositoryError.fetchError`: 조회 중 에러가 발생한 경우
- func fetchAllSummary() async throws -> [AlanSummary]
-}
-
diff --git a/B.READ/B.READ/Sources/Domain/Repository/Stubs/RecordRepositoryStub.swift b/B.READ/B.READ/Sources/Domain/Repository/Stubs/RecordRepositoryStub.swift
index 10958017..fe3c448e 100644
--- a/B.READ/B.READ/Sources/Domain/Repository/Stubs/RecordRepositoryStub.swift
+++ b/B.READ/B.READ/Sources/Domain/Repository/Stubs/RecordRepositoryStub.swift
@@ -9,8 +9,6 @@ import Foundation
actor RecordRepositoryStub: RecordRepository {
-
-
private var storedRecords: [Record] = []
// private var storedRecords: [Record] = DummyData.dummyRecords
@@ -38,7 +36,6 @@ actor RecordRepositoryStub: RecordRepository {
return record
}
-
func fetchRecentReadingRecord(maxCount count: Int) throws -> [Record] {
print("Stub: ", #function)
let records = storedRecords
@@ -48,15 +45,6 @@ actor RecordRepositoryStub: RecordRepository {
return Array(records)
}
-
- func fetchRecordAvailableForSummary() async throws -> Record {
- print("Stub: ", #function)
- guard let record = storedRecords.first(where: { $0.state == .completed && $0.summary == nil }) else {
- throw RepositoryError.dataNotFound
- }
-
- return record
- }
func updateRecord(_ record: Record) throws {
print("Stub: ", #function)
diff --git a/B.READ/B.READ/Sources/Domain/UseCase/LibraryUseCase.swift b/B.READ/B.READ/Sources/Domain/UseCase/LibraryUseCase.swift
index d29f4256..1c3e4696 100644
--- a/B.READ/B.READ/Sources/Domain/UseCase/LibraryUseCase.swift
+++ b/B.READ/B.READ/Sources/Domain/UseCase/LibraryUseCase.swift
@@ -57,13 +57,6 @@ protocol LibraryUseCase {
/// - Returns: [(Record Entity, Book Entity)]
/// - Throws:
/// - `RepositoryError.fetchError`: 데이터 조회 중 에러가 발생한 경우
+ /// - Note: Todo. 몽피
func loadRecentUpdatedReadingRecord(maxCount: Int) async throws -> [(Record, Book)]
-
- /// 최근 업데이트한 `읽기 완료` 상태이면서 요약을 생성하지 않은 독서 기록을 조회합니다.
- ///
- /// - Returns: Record Entity
- /// - Throws:
- /// - `RepositoryError.dataNotFound`: 조회할 독서 기록이 존재하지 않는 경우
- /// - `RepositoryError.fetchError`: 데이터 조회 중 에러가 발생한 경우
- func loadRecentRecordAvailableForSummary() async throws -> Record
}
diff --git a/B.READ/B.READ/Sources/Domain/UseCase/QuoteUseCase.swift b/B.READ/B.READ/Sources/Domain/UseCase/QuoteUseCase.swift
index 1c070235..e1fb8416 100644
--- a/B.READ/B.READ/Sources/Domain/UseCase/QuoteUseCase.swift
+++ b/B.READ/B.READ/Sources/Domain/UseCase/QuoteUseCase.swift
@@ -9,14 +9,27 @@
import Foundation
protocol QuoteUseCase {
- /// 문장을 저장합니다.
+ /// 새로운 문장을 추가합니다.
///
- /// - Parameters:
- /// - memo: 저장할 `Quote` 객체
- /// - record: 문장을 저장할 대상 `Record` 객체
- /// - Throws: `RepositoryError.dataNotFound`, `RepositoryError.dataAlreadyExist`, 저장 중 오류 발생 시
- /// - Note: 이미 존재하는 문장의 경우 업데이트하며, 존재하지 않으면 새로 생성합니다.
- func saveQuote(_ quote: Quote, in record: Record) async throws
+ /// - Parameter:
+ /// - `quote` : 저장할 `Quote` 엔티티. `content`는 공백 제거 후 빈 문자열일 수 없으며, `page`는 해당 도서의 전체 페이지 수(maxPage) 범위 내여야 합니다.
+ /// - `record` : quote가 저장될 독서 기록
+ /// - Throws:
+ /// - `QuoteUseCaseError.emptyContent`: `content`가 빈 문자열이거나 공백만으로 이루어진 경우
+ /// - `QuoteUseCaseError.invalidPage(max:)`: `page`가 유효한 페이지 범위(1...maxPage)를 벗어나는 경우
+ /// - `RepositoryError.dataAlreadyExist`: 동일 ID의 `Quote`가 이미 저장된 경우
+ /// - `RepositoryError.fetchError`: 페이지 유효성 검증 과정에서 저장소 조회 중 에러가 발생한 경우
+ func addQuote(_ quote: Quote, in record: Record) async throws
+
+ /// 기존 문장을 업데이트합니다.
+ ///
+ /// - Parameter quote: 업데이트할 `Quote` 엔티티. `content`와 `page`는 추가 시와 동일하게 검증됩니다.
+ /// - Throws:
+ /// - `QuoteUseCaseError.emptyContent`: `content`가 빈 문자열이거나 공백만으로 이루어진 경우
+ /// - `QuoteUseCaseError.invalidPage(max:)`: `page`가 유효한 페이지 범위를 벗어나는 경우
+ /// - `RepositoryError.dataNotFound`: 수정 대상인 `Quote`가 존재하지 않는 경우
+ /// - `RepositoryError.fetchError`: 페이지 유효성 검증 과정에서 저장소 조회 중 에러가 발생한 경우
+ func updateQuote(_ quote: Quote) async throws
/// 특정 ID의 문장을 삭제합니다.
///
diff --git a/B.READ/B.READ/Sources/Domain/UseCase/SummaryUseCase.swift b/B.READ/B.READ/Sources/Domain/UseCase/SummaryUseCase.swift
deleted file mode 100644
index e3cb64c4..00000000
--- a/B.READ/B.READ/Sources/Domain/UseCase/SummaryUseCase.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-//
-// SummaryUseCase.swift
-// B.READ
-//
-// Created by 김도연 on 6/9/25.
-//
-
-import Foundation
-
-/// 독서 요약 기능을 담당하는 UseCase입니다.
-protocol SummaryUseCase {
-
- /// 사용자가 생성한 요약을 저장합니다.
- ///
- /// - Parameters:
- /// - summary: 저장할 요약 객체
- /// - record: 해당 요약이 속한 독서 기록
- /// - Throws: 저장 실패 시 오류를 발생시킵니다.
- func saveSummary(_ summary: AlanSummary, in record: Record) async throws
-
- /// AI 기반으로 요약을 생성합니다.
- ///
- /// - Parameter record: 요약을 생성할 대상 독서 기록
- /// - Returns: 생성된 요약 객체
- /// - Throws:
- /// - SummaryUseCaseError.promptError: AI 응답이 오류 메시지를 반환한 경우
- /// - SummaryUseCaseError.fatalError: 3회 재시도 후에도 실패한 경우
- func generateSummary(in record: Record) async throws -> AlanSummary
-
- /// 특정 ID에 해당하는 요약을 조회합니다.
- ///
- /// - Parameter id: 조회할 요약의 ID
- /// - Returns: 조회된 요약 객체
- /// - Throws: Repository 또는 UseCase 내부 오류 발생 시
- func fetchSummary(id: String) async throws -> AlanSummary
-
- /// 전체 요약 목록을 조회합니다.
- ///
- /// - Returns: 저장된 모든 요약 리스트
- /// - Throws: 조회 실패 시 오류를 발생시킵니다.
- func fetchAllSummary() async throws -> [AlanSummary]
-}
-
-/// 요약 생성 및 조회 과정에서 발생할 수 있는 오류 타입입니다.
-enum SummaryUseCaseError {
- /// AI로부터 오류 메시지가 반환된 경우
- case promptError(String)
- /// JSON 디코딩 등 파싱 오류
- case parsingError
- /// 3회 재시도에도 실패한 경우
- case fatalError
-}
-
-extension SummaryUseCaseError: LocalizedError {
- var errorDescription: String? {
- switch self {
- case let .promptError(desp):
- desp
- case .parsingError:
- "Parsing error"
- case .fatalError:
- "3번 시도 실패"
- }
- }
-}
-
-/// AI 요약 응답 모델입니다.
-struct ResponseSummary: Decodable {
- let summary: String
- let feelingTags: [String]
-
- enum CodingKeys: String, CodingKey {
- case summary = "Summary"
- case feelingTags
- }
-}
-
-/// AI 오류 응답 모델입니다.
-struct ErrorResponse: Decodable {
- let error: String
-}
diff --git a/B.READ/B.READ/Sources/Domain/UseCaseImpl/LibraryUseCaseImpl.swift b/B.READ/B.READ/Sources/Domain/UseCaseImpl/LibraryUseCaseImpl.swift
index 8a70f1a4..cffaa8f1 100644
--- a/B.READ/B.READ/Sources/Domain/UseCaseImpl/LibraryUseCaseImpl.swift
+++ b/B.READ/B.READ/Sources/Domain/UseCaseImpl/LibraryUseCaseImpl.swift
@@ -9,7 +9,6 @@ import Foundation
final class LibraryUseCaseImpl: LibraryUseCase {
- private let userInfoRepository: UserInfoRepository
private let bookRepository: BookRepository
private let recordRepository: RecordRepository
// private let memoRepository: MemoRepository
@@ -18,13 +17,11 @@ final class LibraryUseCaseImpl: LibraryUseCase {
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
@@ -33,45 +30,32 @@ final class LibraryUseCaseImpl: LibraryUseCase {
func saveRecord(record: Record, book: Book) async throws {
do {
- try Task.checkCancellation()
try await bookRepository.createBook(book)
- try Task.checkCancellation()
-
} catch RepositoryError.dataAlreadyExist {
// 이미 존재하면 무시
print("이미 존재하는 책입니다.")
}
- try Task.checkCancellation()
+
try await recordRepository.createRecord(record)
- try Task.checkCancellation()
- try await self.updateStreakIfNeeded()
}
func editRecord(_ record: Record) async throws {
do {
// 1. 책이 있는지 부터 확인
- try Task.checkCancellation()
let _ = try await bookRepository.fetchBook(isbn: record.isbn)
- try Task.checkCancellation()
} catch RepositoryError.dataNotFound{
// 2. 책이 없으면 책 생성
- try Task.checkCancellation()
let requestBook = try await requestBookDetail(isbn: record.isbn)
- try Task.checkCancellation()
try await bookRepository.createBook(requestBook)
}
do {
// 3. 독서 기록을 수정
- try Task.checkCancellation()
try await recordRepository.updateRecord(record)
} catch RepositoryError.dataNotFound{
// 4. 독서 기록이 없으면 생성
- try Task.checkCancellation()
try await recordRepository.createRecord(record)
}
- try Task.checkCancellation()
- try await self.updateStreakIfNeeded()
}
func loadRecord(_ recordID: String) async throws -> (Record, Book) {
@@ -155,10 +139,6 @@ final class LibraryUseCaseImpl: LibraryUseCase {
// 5. 최종적으로 (Record, Book) 쌍을 반환
return pairsItems
}
-
- func loadRecentRecordAvailableForSummary() async throws -> Record {
- return try await recordRepository.fetchRecordAvailableForSummary()
- }
}
private extension LibraryUseCaseImpl {
@@ -177,22 +157,4 @@ private extension LibraryUseCaseImpl {
try await bookRepository.createBook(book)
return book
}
-
- func updateStreakIfNeeded() async throws {
- let currentTime: Date = .now
-
- var userInfo = try await userInfoRepository.fetchUserInfo()
- if userInfo.lastStreakUpdatedAt.isSameDay(as: currentTime) {
- return
- }
-
- if !userInfo.lastStreakUpdatedAt.isInCurrentWeek {
- userInfo.streak = userInfo.streak.map { DailyStatus(weekday: $0.weekday, isCompleted: false) }
- }
-
- userInfo.streak[currentTime.weekdayInt - 1] = DailyStatus(weekday: currentTime.weekdayInt - 1, isCompleted: true)
- userInfo.lastStreakUpdatedAt = currentTime
-
- try await userInfoRepository.updateUserInfo(userInfo)
- }
}
diff --git a/B.READ/B.READ/Sources/Domain/UseCaseImpl/MemoUseCaseImpl.swift b/B.READ/B.READ/Sources/Domain/UseCaseImpl/MemoUseCaseImpl.swift
index d6535bd1..7e3bd79e 100644
--- a/B.READ/B.READ/Sources/Domain/UseCaseImpl/MemoUseCaseImpl.swift
+++ b/B.READ/B.READ/Sources/Domain/UseCaseImpl/MemoUseCaseImpl.swift
@@ -9,18 +9,11 @@ import Foundation
final class MemoUseCaseImpl: MemoUseCase {
- let userInfoRepository: UserInfoRepository
let bookRepository: BookRepository
let memoRepository: MemoRepository
let aiService: AIService
- init(
- userInfoRepository: UserInfoRepository,
- bookRepository: BookRepository,
- memoRepository: MemoRepository,
- aiService: AIService
- ) {
- self.userInfoRepository = userInfoRepository
+ init(bookRepository: BookRepository, memoRepository: MemoRepository, aiService: AIService) {
self.bookRepository = bookRepository
self.memoRepository = memoRepository
self.aiService = aiService
@@ -32,8 +25,6 @@ final class MemoUseCaseImpl: MemoUseCase {
} catch RepositoryError.dataNotFound {
try await memoRepository.createMemo(memo, in: record)
}
-
- try await self.updateStreakIfNeeded()
}
func fetchMemo(id: String) async throws -> Memo {
@@ -92,21 +83,3 @@ final class MemoUseCaseImpl: MemoUseCase {
return try await bookRepository.fetchBook(isbn: isbn).name
}
}
-
-private extension MemoUseCaseImpl {
- func updateStreakIfNeeded() async throws {
- let currentTime: Date = .now
-
- var userInfo = try await userInfoRepository.fetchUserInfo()
- if userInfo.lastStreakUpdatedAt.isSameDay(as: currentTime) { return }
-
- if !userInfo.lastStreakUpdatedAt.isInCurrentWeek {
- userInfo.streak = userInfo.streak.map { DailyStatus(weekday: $0.weekday, isCompleted: false) }
- }
-
- userInfo.streak[currentTime.weekdayInt - 1] = DailyStatus(weekday: currentTime.weekdayInt - 1, isCompleted: true)
- userInfo.lastStreakUpdatedAt = currentTime
-
- try await userInfoRepository.updateUserInfo(userInfo)
- }
-}
diff --git a/B.READ/B.READ/Sources/Domain/UseCaseImpl/ProfileUseCaseImpl.swift b/B.READ/B.READ/Sources/Domain/UseCaseImpl/ProfileUseCaseImpl.swift
index 5bb57f33..570f2fcd 100644
--- a/B.READ/B.READ/Sources/Domain/UseCaseImpl/ProfileUseCaseImpl.swift
+++ b/B.READ/B.READ/Sources/Domain/UseCaseImpl/ProfileUseCaseImpl.swift
@@ -120,7 +120,7 @@ extension ProfileUseCaseImpl {
categories: [],
recentKeywords: [],
generateCount: 0,
- lastStreakUpdatedAt: Date().addingTimeInterval(-24 * 60 * 60),
+ lastStreakUpdatedAt: .now,
streak: (0...6).map { DailyStatus(weekday: $0, isCompleted: false) }
)
diff --git a/B.READ/B.READ/Sources/Domain/UseCaseImpl/QuoteUseCaseImpl.swift b/B.READ/B.READ/Sources/Domain/UseCaseImpl/QuoteUseCaseImpl.swift
index a9e2f2d6..9cbcab4c 100644
--- a/B.READ/B.READ/Sources/Domain/UseCaseImpl/QuoteUseCaseImpl.swift
+++ b/B.READ/B.READ/Sources/Domain/UseCaseImpl/QuoteUseCaseImpl.swift
@@ -6,10 +6,9 @@
//
import Foundation
+import WidgetKit
final class QuoteUseCaseImpl: QuoteUseCase {
-
- private let userInfoRepository: UserInfoRepository
private let quoteRepository: QuoteRepository
private let bookRepository: BookRepository
@@ -17,28 +16,27 @@ final class QuoteUseCaseImpl: QuoteUseCase {
/// - Parameters:
/// - quoteRepo: 문장 저장소 구현체
/// - bookRepo: 도서 저장소 구현체(페이지 검증용)
- init(
- userInfoRepository: UserInfoRepository,
- quoteRepository: QuoteRepository,
- bookRepository: BookRepository
- ) {
- self.userInfoRepository = userInfoRepository
+ init(quoteRepository: QuoteRepository, bookRepository: BookRepository) {
self.quoteRepository = quoteRepository
self.bookRepository = bookRepository
}
- func saveQuote(_ quote: Quote, in record: Record) async throws {
- do {
- try await quoteRepository.updateQuote(quote)
- } catch RepositoryError.dataNotFound {
- try await quoteRepository.createQuote(quote, in: record)
- }
-
- try await self.updateStreakIfNeeded()
+ func addQuote(_ quote: Quote, in record: Record) async throws {
+ // 저장 수행
+ try await quoteRepository.createQuote(quote, in: record)
+ try await syncSharedDefaults()
+ }
+
+ func updateQuote(_ quote: Quote) async throws {
+ // 업데이트 수행
+ try await quoteRepository.updateQuote(quote)
+ try await syncSharedDefaults()
}
func removeQuote(id: String) async throws {
+ // 삭제 수행
try await quoteRepository.deleteQuote(id: id)
+ try await syncSharedDefaults()
}
func fetchQuote(id: String) async throws -> Quote {
@@ -52,31 +50,43 @@ final class QuoteUseCaseImpl: QuoteUseCase {
func fetchAllQuotes() async throws -> [Quote] {
return try await quoteRepository.fetchAllQuotes()
}
-
// TODO: - 조회한 도서가 없을 경우 알라딘 검색 후 도서 저장 -> 도서 제목 반환
func loadBookTitle(_ isbn: String) async throws -> String {
return try await bookRepository.fetchBook(isbn: isbn).name
}
-}
-
-private extension QuoteUseCaseImpl {
- func updateStreakIfNeeded() async throws {
- let currentTime: Date = .now
-
- var userInfo = try await userInfoRepository.fetchUserInfo()
- // 같은 날짜에 이미 업데이트가 이루어졌다면 return
- if userInfo.lastStreakUpdatedAt.isSameDay(as: currentTime) { return }
+
+ /// 모든 `Quote` → `SharedQuote` 로 변환한 뒤
+ /// App Group(UserDefaults) 에 저장하고 위젯 타임라인을 즉시 갱신.
+ ///
+ /// 1. 저장소에서 모든 Quote 를 가져옴.
+ /// 2. ISBN → 책 제목을 비동기로 조회하여 `SharedQuote` 생성
+ /// 3. `SharedQuotesStore.save(_:)` 호출로 JSON 덤프
+ /// 4. `WidgetCenter.shared.reloadAllTimelines()` 로 위젯 갱신
+ ///
+ /// - Throws:
+ /// - `RepositoryError.fetchError` : Quote 또는 Book 로드 과정에서 발생
+ /// - `EncodingError` : JSON 인코딩 실패
+ private func syncSharedDefaults() async throws {
+ let allQuotes = try await quoteRepository.fetchAllQuotes()
- // 스트릭이 이번주의 첫 스트릭일 경우 초기화
- if !userInfo.lastStreakUpdatedAt.isInCurrentWeek {
- userInfo.streak = userInfo.streak.map { DailyStatus(weekday: $0.weekday, isCompleted: false) }
+ let sharedQuotes: [SharedQuote] = try await withThrowingTaskGroup(of: SharedQuote.self) { group in
+ for quote in allQuotes {
+ group.addTask {
+ let title = try? await self.loadBookTitle(quote.isbn)
+ return SharedQuote(
+ id: quote.id,
+ content: quote.content,
+ bookTitle: title ?? "" // 제목 없으면 빈 문자열
+ )
+ }
+ }
+ return try await group.reduce(into: []) { $0.append($1) }
}
- // 업데이트
- userInfo.streak[currentTime.weekdayInt - 1] = DailyStatus(weekday: currentTime.weekdayInt - 1, isCompleted: true)
- userInfo.lastStreakUpdatedAt = currentTime
+ try SharedQuotesStore.save(sharedQuotes)
- try await userInfoRepository.updateUserInfo(userInfo)
+ // 새로운 데이터가 쓰였으니 위젯 타임라인 즉시 갱신
+ WidgetCenter.shared.reloadAllTimelines()
}
}
diff --git a/B.READ/B.READ/Sources/Domain/UseCaseImpl/SummaryUseCaseImpl.swift b/B.READ/B.READ/Sources/Domain/UseCaseImpl/SummaryUseCaseImpl.swift
deleted file mode 100644
index 7190cc26..00000000
--- a/B.READ/B.READ/Sources/Domain/UseCaseImpl/SummaryUseCaseImpl.swift
+++ /dev/null
@@ -1,150 +0,0 @@
-//
-// SummaryUseCaseImpl.swift
-// B.READ
-//
-// Created by 김도연 on 6/9/25.
-//
-
-import Foundation
-
-final class SummaryUseCaseImpl: SummaryUseCase {
-
- let userInfoRepository: UserInfoRepository
- let summaryRepository: SummaryRepository
- let bookRepository: BookRepository
- let recordRepository: RecordRepository
- let aiService: AIService
-
- init(
- userInfoRepository: UserInfoRepository,
- summaryRepository: SummaryRepository,
- bookRepository: BookRepository,
- recordRepository: RecordRepository,
- aiService: AIService
- ) {
- self.userInfoRepository = userInfoRepository
- self.summaryRepository = summaryRepository
- self.bookRepository = bookRepository
- self.recordRepository = recordRepository
- self.aiService = aiService
- }
-
- func saveSummary(_ summary: AlanSummary, in record: Record) async throws {
- try Task.checkCancellation()
- try await summaryRepository.createSummary(summary, in: record)
- try Task.checkCancellation()
- try await self.updateStreakIfNeeded()
- }
-
- func generateSummary(in record: Record) async throws -> AlanSummary {
- try Task.checkCancellation()
- let book = try await bookRepository.fetchBook(isbn: record.isbn)
- try Task.checkCancellation()
- let prefixPrompt = """
- You are an AI that generates a reading reflection summary based on the following information.
- Book title: \(book.name), Author: \(book.author)
- When the user provides these details plus their collected quotes and memos, reply only with a valid JSON object matching this schema:
- {"Summary": string, "feelingTags": [ string ] }
-
- Rules:
- 1. Your entire reply must be a single JSON object parsable by a standard JSON parser.
- 2. The very first character of your response must be { and the very last character must be }.
- 3. Do not wrap the JSON in any quotes, markdown fences, or extra fields (e.g. no "action" wrapper).
- 4. Do not include any keys other than "Summary", "feelingTags".
- 5. Extract exactly five human emotion tags for "feelingTags".
- 6. The "Summary" field must include all user-provided memos and be as detailed and lengthy as possible.
- 7. If you cannot fulfill the request, respond with:
- {
- "error": "description of the problem"
- }
-
- User’s memos:
- """
-
- var attempt = 0
-
- while attempt < 3 {
- let memoContent = trimmedMemoContent(from: record.memos, retryCount: attempt)
- let prompt = prefixPrompt + memoContent
- try Task.checkCancellation()
- let jsonString = try await aiService.request(prompt: prompt)
- try Task.checkCancellation()
- let data = Data(jsonString.utf8)
-
- do {
- let contents = try JSONDecoder().decode(ResponseSummary.self, from: data)
- let tags = contents.feelingTags.map { Tag($0) }
-
- return AlanSummary(
- id: UUID().uuidString,
- isbn: book.isbn,
- content: contents.summary,
- tags: tags,
- createdAt: .now
- )
- } catch {
- do {
- let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: data)
- throw SummaryUseCaseError.promptError(errorResponse.error)
- } catch {
- attempt += 1
- }
- }
- }
-
- throw SummaryUseCaseError.fatalError
- }
-
-
- func fetchSummary(id: String) async throws -> AlanSummary {
- try Task.checkCancellation()
- let summary = try await summaryRepository.fetchSummary(id: id)
- try Task.checkCancellation()
-
- return summary
- }
-
- func fetchAllSummary() async throws -> [AlanSummary] {
- try Task.checkCancellation()
- let summaries = try await summaryRepository.fetchAllSummary()
- try Task.checkCancellation()
-
- return summaries
- }
-
-}
-
-private extension SummaryUseCaseImpl {
- /// memo 리스트에서 뒤에서부터 N개 제거한 뒤, 하나의 문자열로 병합
- ///
- /// - Parameters:
- /// - memos: 원본 Memo 리스트
- /// - retryCount: 제거할 메모 개수
- /// - Returns: 연결된 메모 문자열
- func trimmedMemoContent(from memos: [Memo], retryCount: Int) -> String {
- guard retryCount < memos.count else {
- return ""
- }
- let trimmed = Array(memos.prefix(memos.count - retryCount))
- return trimmed.map { $0.content }.joined(separator: " ")
- }
-
- func updateStreakIfNeeded() async throws {
- let currentTime: Date = .now
-
- var userInfo = try await userInfoRepository.fetchUserInfo()
- // 같은 날짜에 이미 업데이트가 이루어졌다면 return
- if userInfo.lastStreakUpdatedAt.isSameDay(as: currentTime) { return }
-
- // 스트릭이 이번주의 첫 스트릭일 경우 초기화
- if !userInfo.lastStreakUpdatedAt.isInCurrentWeek {
- userInfo.streak = userInfo.streak.map { DailyStatus(weekday: $0.weekday, isCompleted: false) }
- }
-
- // 업데이트
- userInfo.streak[currentTime.weekdayInt - 1] = DailyStatus(weekday: currentTime.weekdayInt - 1, isCompleted: true)
- userInfo.lastStreakUpdatedAt = currentTime
-
- try await userInfoRepository.updateUserInfo(userInfo)
- }
-}
diff --git a/B.READ/B.READ/Sources/Network/Aladin/AladinRouter.swift b/B.READ/B.READ/Sources/Network/Aladin/AladinRouter.swift
index 514ddd12..3dcd8c53 100644
--- a/B.READ/B.READ/Sources/Network/Aladin/AladinRouter.swift
+++ b/B.READ/B.READ/Sources/Network/Aladin/AladinRouter.swift
@@ -80,7 +80,7 @@ enum AladinRouter: RequestConvertible {
)!
components.queryItems = queryItems
-// print("[📡 Aladin URL]:", components.url?.absoluteString ?? "❌ URL 생성 실패")
+ print("[📡 Aladin URL]:", components.url?.absoluteString ?? "❌ URL 생성 실패")
var request = URLRequest(url: components.url!)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
diff --git a/B.READ/B.READ/Sources/Network/NetworkClient/NetworkClient.swift b/B.READ/B.READ/Sources/Network/NetworkClient/NetworkClient.swift
index 4973cae7..1c1d5a1a 100644
--- a/B.READ/B.READ/Sources/Network/NetworkClient/NetworkClient.swift
+++ b/B.READ/B.READ/Sources/Network/NetworkClient/NetworkClient.swift
@@ -49,8 +49,6 @@ class NetworkClient {
throw URLError(.badServerResponse)
}
-// print(String(data: data, encoding: .utf8) ?? "데이터를 문자열로 변환할 수 없습니다.")
-
let decoded = try JSONDecoder().decode(T.self, from: data)
return (decoded, httpResponse)
}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/InfoView.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/InfoView.swift
deleted file mode 100644
index e29f14c1..00000000
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/InfoView.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-//
-// InfoView.swift
-// B.READ
-//
-// Created by 김도연 on 6/10/25.
-//
-
-import SwiftUI
-
-// MARK: - (S)InfoView
-struct InfoView: View {
- let layoutPadding : CGFloat = 16
- let horizontalPadding : CGFloat = 24
- let title : String
- var content: String
-
- var body: some View {
- VStack(alignment: .leading, spacing: layoutPadding) {
- Text(title)
- .brStyleFont(.pretendard(.semiBold, size: 16), lineHeight: 1.2, letterSpacing: 0.02)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- InnerContentView(content: content)
- }
- .padding(.horizontal, horizontalPadding)
- .padding(.vertical, layoutPadding)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(.white)
- }
-}
-
-#Preview {
- InfoView(title: "📚 요약", content: "Lorem ipsum dolor sit amet consectetur. Nec neque non sit nulla elit dis morbi sem gravida. Sit semper varius leo sit amet nec ut egestas sapien. At interdum integer consequat at. Proin sit ut venenatis vestibulum maecenas at fermentum. Lorem ipsum dolor sit amet consectetur. Nec neque non sit nulla elit dis morbi sem gravida. Sit semper varius leo sit amet nec ut egestas sapien. At interdum integer consequat at. Proin sit ut venenatis vestibulum maecenas at fermentum.")
-
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/InnerContentView.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/InnerContentView.swift
deleted file mode 100644
index e543a9d8..00000000
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/InnerContentView.swift
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// InnerContentView.swift
-// B.READ
-//
-// Created by 김도연 on 6/10/25.
-//
-
-import SwiftUI
-
-// MARK: - (S)InnerContentView
-struct InnerContentView: View {
- var content: String
-
- var body: some View {
- Text(content)
- .brStyleFont(.pretendard(.light, size: 14), lineHeight: 1.2, letterSpacing: -0.025)
- .multilineTextAlignment(.leading)
- .frame(maxWidth: .infinity, alignment: .leading)
- }
-}
-
-#Preview {
- InnerContentView(content: "Lorem ipsum dolor sit amet consectetur. Nec neque non sit nulla elit dis morbi sem gravida. Sit semper varius leo sit amet nec ut egestas sapien. At interdum integer consequat at. Proin sit ut venenatis vestibulum maecenas at fermentum. Lorem ipsum dolor sit amet consectetur. Nec neque non sit nulla elit dis morbi sem gravida. Sit semper varius leo sit amet nec ut egestas sapien. At interdum integer consequat at. Proin sit ut venenatis vestibulum maecenas at fermentum.")
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/LogoView.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/LogoView.swift
deleted file mode 100644
index 9a7f2a81..00000000
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/LogoView.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-//
-// LogoView.swift
-// B.READ
-//
-// Created by 신승재 on 6/9/25.
-//
-
-import SwiftUI
-
-struct LogoView: View {
- var body: some View {
- Image(.topLogo)
- .resizable()
- .renderingMode(.template)
- .aspectRatio(contentMode: .fit)
- .foregroundColor(.orange3)
- .frame(height: 25)
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.vertical, 8)
- .padding(.leading, 24)
- }
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/MemoCell.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/MemoCell.swift
index 35dc4a34..8395985a 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/MemoCell.swift
+++ b/B.READ/B.READ/Sources/Presentation/Common/Components/MemoCell.swift
@@ -10,7 +10,6 @@ import SwiftUI
struct MemoCell: View {
let content: String
- let highlight: String?
let date: Date
let startPage: Int
let endPage: Int
@@ -18,14 +17,12 @@ struct MemoCell: View {
init(
content: String,
- highlight: String? = nil,
date: Date,
startPage: Int,
endPage: Int,
action: (() -> Void)? = nil
) {
self.content = content
- self.highlight = highlight
self.date = date
self.startPage = startPage
self.endPage = endPage
@@ -34,18 +31,11 @@ struct MemoCell: View {
var body: some View {
VStack(spacing: 8) {
- Group {
- if let keyword = self.highlight, !keyword.isEmpty {
- content.highlightedText(keyword: keyword)
- }
- else {
- Text(content)
- .font(Font(UIFont.pretendard(.regular, size: 16)))
- .foregroundColor(.black)
- }
- }
- .brStyle(.pretendard(.regular, size: 16), lineHeight: 1.3)
- .frame(maxWidth: .infinity, alignment: .leading)
+ Text(content)
+ .brStyleFont(.pretendard(.regular, size: 16), lineHeight: 1.3)
+ .foregroundStyle(.black)
+ .frame(maxWidth: .infinity, alignment: .leading)
+
HStack(spacing: 4) {
@@ -62,7 +52,7 @@ struct MemoCell: View {
.frame(maxWidth: .infinity)
}
.padding(16)
- .background(.brown2.opacity(0.3))
+ .background(.brown4.opacity(0.3))
.clipShape(
RoundedRectangle(cornerRadius: 16)
)
@@ -91,5 +81,4 @@ Lorem ipsum dolor sit amet con sect etur. Aug ue po tenti au ctor faci lisi ult
MemoCell(content: content, date: Date(), startPage: 2, endPage: 4) {
print("hello")
}
- MemoCell(content: content, highlight: "Lorem", date: .now, startPage: 2, endPage: 4)
}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/MultiInfoView.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/MultiInfoView.swift
deleted file mode 100644
index f86eec8a..00000000
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/MultiInfoView.swift
+++ /dev/null
@@ -1,45 +0,0 @@
-//
-// MultiInfoView.swift
-// B.READ
-//
-// Created by 김도연 on 6/10/25.
-//
-
-import SwiftUI
-
-// MARK: - (S)MultiInfoView
-struct MultiInfoView: View {
- let layoutPadding : CGFloat = 16
- let horizontalPadding : CGFloat = 24
- let title : String
- var content: [String]
-
- var body: some View {
- VStack(alignment: .leading, spacing: layoutPadding) {
- Text(title)
- .brStyleFont(.pretendard(.semiBold, size: 16), lineHeight: 1.2, letterSpacing: 0.02)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- VStack(alignment: .leading, spacing: 12) {
- ForEach(content.indices, id: \.self) { index in
- InnerContentView(content: "• \(content[index])")
- }
- }
- }
- .padding(.horizontal, horizontalPadding)
- .padding(.vertical, layoutPadding)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(.white)
- }
-}
-
-#Preview {
- MultiInfoView(
- title: "🍞 문장",
- content: [
- "Lorem ipsum dolor sit amet consectetur. Nec neque non sit nulla eli",
- "Lorem ipsum dolor sit amet consectetur. Nec neque non sit nulla eli",
- "Lorem ipsum dolor sit amet consectetur. Nec neque non sit nulla eli",
- "Lorem ipsum dolor sit amet consectetur. Nec neque non sit nulla eli"
- ])
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/QuoteCell.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/QuoteCell.swift
index cb80f08f..805287c4 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/QuoteCell.swift
+++ b/B.READ/B.READ/Sources/Presentation/Common/Components/QuoteCell.swift
@@ -10,6 +10,7 @@ import SwiftUI
enum ColorTone {
case soft
case regular
+ case strong
var color: Color {
switch self {
@@ -17,12 +18,14 @@ enum ColorTone {
.green1
case .regular:
.green2
+ case .strong:
+ .green6
}
}
static func tone(isbn: String) -> ColorTone {
let hash = abs(isbn.hash)
- let tones: [ColorTone] = [.soft, .regular]
+ let tones: [ColorTone] = [.soft, .regular, .strong]
return tones[hash % tones.count]
}
}
@@ -30,20 +33,12 @@ enum ColorTone {
struct QuoteCell: View {
let content: String
- let highlight: String?
let page: Int
let colorTone: ColorTone
let action: (() -> Void)?
- init(
- content: String,
- highlight: String? = nil,
- page: Int,
- colorTone: ColorTone,
- action: (() -> Void)? = nil
- ) {
+ init(content: String, page: Int, colorTone: ColorTone, action: (() -> Void)? = nil) {
self.content = content
- self.highlight = highlight
self.page = page
self.colorTone = colorTone
self.action = action
@@ -51,22 +46,14 @@ struct QuoteCell: View {
var body: some View {
VStack(spacing: 8) {
- Group {
- if let keyword = self.highlight, !keyword.isEmpty {
- content.highlightedText(keyword: keyword)
- }
- else {
- Text(content)
- .font(Font(UIFont.pretendard(.regular, size: 16)))
- .foregroundColor(.black)
- }
- }
- .brStyle(.pretendard(.regular, size: 16), lineHeight: 1.3)
- .frame(maxWidth: .infinity, alignment: .leading)
+ Text(content)
+ .foregroundStyle(colorTone == .strong ? .backgroundDefault : .black)
+ .brStyleFont(.pretendard(.regular, size: 16), lineHeight: 1.3)
+ .frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 4) {
Text("\(page)쪽")
- .foregroundStyle(.gray7)
+ .foregroundStyle(colorTone == .strong ? .green1 : .gray7)
if action != nil { menuButton() }
}
.brStyleFont(.pretendard(.light, size: 14), lineHeight: 1, letterSpacing: 0.02)
@@ -92,7 +79,7 @@ struct QuoteCell: View {
.frame(width: 16, height: 16)
.rotationEffect(.degrees(90))
}
- .foregroundStyle(.gray7)
+ .foregroundStyle(colorTone == .strong ? .green1 : .gray7)
}
}
@@ -100,9 +87,7 @@ struct QuoteCell: View {
let content = """
가나다라마문장을 캡쳐해볼게요. 이건 제가 수집한 문장이에요ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
"""
- QuoteCell(content: content, page: 28, colorTone: .regular) {
+ QuoteCell(content: content, page: 28, colorTone: .strong) {
print("action")
}
- QuoteCell(content: content, highlight: "가나다라마", page: 28, colorTone: .regular)
- QuoteCell(content: content, highlight: "수집한", page: 28, colorTone: .soft)
}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/SearchBar.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/SearchBar.swift
index 5f473248..4d35625b 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/SearchBar.swift
+++ b/B.READ/B.READ/Sources/Presentation/Common/Components/SearchBar.swift
@@ -27,8 +27,8 @@ enum SearchBarStyle {
var frameSize: CGSize {
switch self {
- case .default: return CGSize(width: 0, height: 48)
- case .compact: return CGSize(width: 0, height: 36)
+ case .default: return CGSize(width: 282, height: 48)
+ case .compact: return CGSize(width: 275, height: 36)
}
}
}
@@ -81,8 +81,7 @@ struct SearchBar: View {
.background(.clear)
.padding(.trailing, layoutPadding)
}
- .frame(maxWidth: .infinity)
- .frame(height: style.frameSize.height)
+ .frame(width: style.frameSize.width, height: style.frameSize.height)
.roundedBackground()
}
}
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..526d9317 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/SortMenu.swift
+++ b/B.READ/B.READ/Sources/Presentation/Common/Components/SortMenu.swift
@@ -22,13 +22,13 @@ struct SortMenu: View {
HStack(spacing: 4) {
Text(isOpened ? "정렬 기준" : selectedOption.rawValue)
.frame(maxWidth: .infinity, alignment: .center)
- .animation(.easeInOut(duration: 0.3), value: isOpened)
-
- Image(systemName: SFSymbol.chevronCompactDown.name)
- .resizable()
- .frame(width: 10 , height: 5, alignment: .trailing)
- .rotationEffect(.degrees(isOpened ? -180 : 0)) // 위/아래 전환
- .animation(.easeInOut(duration: 0.3), value: isOpened)
+ Image(
+ systemName: isOpened
+ ? SFSymbol.chevronCompactUp.name
+ : SFSymbol.chevronCompactDown.name
+ )
+ .resizable()
+ .frame(width: 10 , height: 5, alignment: .trailing)
} // : HStack
.brStyleFont(.pretendard(.medium, size: 12), lineHeight: 1, letterSpacing: -0.02)
.foregroundStyle(.gray2)
@@ -69,6 +69,5 @@ struct SortMenu: View {
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 6))
.shadow(color: .gray2.opacity(0.3), radius: 30, x: 0, y: 2)
- .transition(.scale.combined(with: .opacity))
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/Components/TopTabBar.swift b/B.READ/B.READ/Sources/Presentation/Common/Components/TopTabBar.swift
index 4e1b2dec..2a12b6cd 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/Components/TopTabBar.swift
+++ b/B.READ/B.READ/Sources/Presentation/Common/Components/TopTabBar.swift
@@ -17,12 +17,6 @@ struct TabItem {
self.selectedImage = selectedImage
self.unselectedImage = unselectedImage
}
-
- init(title: String, image: Image? = nil) {
- self.title = title
- self.selectedImage = image
- self.unselectedImage = image
- }
}
// MARK: - (S)TopTabBar
@@ -96,10 +90,10 @@ private struct HeaderView: View {
// MARK: (F)imageLabel
@ViewBuilder
private func imageLabel(index: Int, isSelected: Bool) -> some View {
- if let image = isSelected ? tabs[index].selectedImage : tabs[index].unselectedImage {
- image
- .renderingMode(.template) // 반드시 필요함: 색상 적용을 위해
- .foregroundStyle(isSelected ? .brown7 : .gray2)
+ if isSelected {
+ tabs[index].selectedImage
+ } else {
+ tabs[index].unselectedImage
}
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/MainTabView.swift b/B.READ/B.READ/Sources/Presentation/Common/MainTabView.swift
index d24b5bfb..d0394208 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/MainTabView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Common/MainTabView.swift
@@ -7,13 +7,13 @@
import SwiftUI
-enum Tab {
- case home, search, library, record, mypage
-}
-
struct MainTabView: View {
@State private var selectedTab: Tab = .home
+ enum Tab {
+ case home, search, library, record, mypage
+ }
+
init() {
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
@@ -26,14 +26,20 @@ struct MainTabView: View {
var body: some View {
CoordinatorContainer {
TabView(selection: $selectedTab) {
- HomeView(selectedTab: $selectedTab)
+
+ HomeView()
.tabItem {
Image(systemName: SFSymbol.house.name)
Text("홈")
}
.tag(Tab.home)
-
- SearchView()
+
+ SearchView(
+ inputViewModel: SearchInputViewModel(),
+ resultViewModel: SearchResultViewModel(),
+ recentSearchViewModel: RecentSearchViewModel(),
+ bestSellerViewModel: BestSellerViewModel()
+ )
.tabItem {
Image(systemName: SFSymbol.magnify.name)
Text("검색")
@@ -66,7 +72,5 @@ struct MainTabView: View {
}
#Preview {
- PreviewableContainer {
- MainTabView()
- }
+ MainTabView()
}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/ValueObject/MemoVO.swift b/B.READ/B.READ/Sources/Presentation/Common/ValueObject/MemoVO.swift
index d1ff34b4..741f0898 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/ValueObject/MemoVO.swift
+++ b/B.READ/B.READ/Sources/Presentation/Common/ValueObject/MemoVO.swift
@@ -15,15 +15,6 @@ struct MemoGroup: Identifiable {
var memos: [MemoVO]
}
-extension MemoGroup: Equatable {
- static func == (lhs: MemoGroup, rhs: MemoGroup) -> Bool {
- return lhs.id == rhs.id &&
- lhs.isbn == rhs.isbn &&
- lhs.bookTitle == rhs.bookTitle &&
- lhs.memos == rhs.memos
- }
-}
-
// MARK: - (S)MemoVO
struct MemoVO: Identifiable {
let id: String
diff --git a/B.READ/B.READ/Sources/Presentation/Common/ValueObject/QuoteVO.swift b/B.READ/B.READ/Sources/Presentation/Common/ValueObject/QuoteVO.swift
index 25c8cf6c..12af0623 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/ValueObject/QuoteVO.swift
+++ b/B.READ/B.READ/Sources/Presentation/Common/ValueObject/QuoteVO.swift
@@ -15,22 +15,12 @@ struct QuoteGroup: Identifiable {
var quotes: [QuoteVO]
}
-extension QuoteGroup: Equatable {
- static func == (lhs: QuoteGroup, rhs: QuoteGroup) -> Bool {
- return lhs.id == rhs.id &&
- lhs.isbn == rhs.isbn &&
- lhs.bookTitle == rhs.bookTitle &&
- lhs.quotes == rhs.quotes
- }
-}
-
// MARK: - (S)QuoteVO
-/// - NOTE: content와 page는 수정을 통해 변경될 수 있음
struct QuoteVO: Identifiable, Hashable {
let id: String
let isbn: String
- var content: String
- var page: Int
+ let content: String
+ let page: Int
let record: RecordDetailVO
init(id: String, isbn: String, content: String, page: Int, record: RecordDetailVO) {
diff --git a/B.READ/B.READ/Sources/Presentation/Common/ValueObject/SummaryVO.swift b/B.READ/B.READ/Sources/Presentation/Common/ValueObject/SummaryVO.swift
deleted file mode 100644
index 3a54226d..00000000
--- a/B.READ/B.READ/Sources/Presentation/Common/ValueObject/SummaryVO.swift
+++ /dev/null
@@ -1,62 +0,0 @@
-//
-// SummaryVO.swift
-// B.READ
-//
-// Created by 김도연 on 6/10/25.
-//
-
-import Foundation
-
-// MARK: - (S)SummaryVO
-struct SummaryVO: Identifiable {
- let id: String
- let isbn: String
- let content: String
- let tags: [TagVO]
- let createdAt: Date
-
- init(id: String, isbn: String, content: String, tags: [TagVO], createdAt: Date) {
- self.id = id
- self.isbn = isbn
- self.content = content
- self.tags = tags
- self.createdAt = createdAt
- }
-
- init(_ summary: AlanSummary) {
- self.init(
- id: summary.id,
- isbn: summary.isbn,
- content: summary.content,
- tags: summary.tags.map{ TagVO($0) },
- createdAt: summary.createdAt
- )
- }
-}
-
-// MARK: - (S)TagVO
-struct TagVO: Identifiable, Hashable {
- let id: String
- let content: String
-
- init(id: String, content: String) {
- self.id = id
- self.content = content
- }
-
- init(_ tag: Tag) {
- self.init(
- id: tag.id,
- content: tag.content
- )
- }
-
- func hash(into hasher: inout Hasher) {
- hasher.combine(id)
- }
-
- static func == (lhs: TagVO, rhs: TagVO) -> Bool {
- return lhs.id == rhs.id &&
- lhs.content == rhs.content
- }
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Home/HomeView.swift b/B.READ/B.READ/Sources/Presentation/Home/HomeView.swift
index 6f433674..2f4e98f1 100644
--- a/B.READ/B.READ/Sources/Presentation/Home/HomeView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Home/HomeView.swift
@@ -8,37 +8,26 @@
import SwiftUI
struct HomeView: View {
- @EnvironmentObject private var coordinator: Coordinator
+
@StateObject private var viewModel = HomeViewModel()
- @Binding var selectedTab: Tab
var body: some View {
- VStack(spacing: 0) {
+ ScrollView(showsIndicators: false) {
+ BreadGuideView()
+ .padding(.top, 24)
- LogoView()
+ RecentBookSectionView(viewModel: viewModel)
+ .padding(.top, 24)
- ScrollView(showsIndicators: false) {
- BreadGuideView(
- coordinator: coordinator,
- selectedTab: $selectedTab,
- viewModel: viewModel
- )
- .padding(.top, 24)
-
- RecentBookSectionView(viewModel: viewModel)
+ if viewModel.bestSellerList.count > 1 {
+ RecommandSectionView(bookList: viewModel.bestSellerList[0])
.padding(.top, 24)
-
- if viewModel.bestSellerList.count > 1 {
- RecommandSectionView(bookList: viewModel.bestSellerList[0])
- .padding(.top, 24)
-
- RecommandSectionView(bookList: viewModel.bestSellerList[1])
- }
+
+ RecommandSectionView(bookList: viewModel.bestSellerList[1])
}
}
.frame(maxWidth: .infinity, alignment: .top)
.background(.backgroundDefault)
- .scrollIndicators(.hidden)
.onAppear {
viewModel.send(.onAppear)
}
@@ -50,31 +39,25 @@ struct HomeView: View {
// MARK: - (S)BreadGuideView
private struct BreadGuideView: View {
-
- @ObservedObject var coordinator: Coordinator
- @Binding var selectedTab: Tab
- let viewModel: HomeViewModel
- var hasAvailableSummaryRecord: Bool {
- viewModel.availableSummaryRecordId != nil
- }
-
var body: some View {
HStack(alignment: .top, spacing: 16.5) {
- Image(hasAvailableSummaryRecord ? .happyBread : .sadBread)
+ Image(.happyBread)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 109)
- talkBubble()
- .onTapGesture {
- if let recordId = viewModel.availableSummaryRecordId {
- selectedTab = .library
- coordinator.push(.libraryDetail(id: recordId))
- } else {
- selectedTab = .library
- }
- }
+ VStack(alignment: .trailing, spacing: 5) {
+ Text("빵식이가 요약할 수 있는 책이 있어요!")
+ .foregroundStyle(.gray9)
+ .brStyleFont(.pretendard(.semiBold, size: 13), lineHeight: 1.3, letterSpacing: 0.02)
+
+ talkBubble()
+ }
+ .padding(.vertical, 8)
+ .padding(.horizontal, 16)
+ .background(.brown4.opacity(0.4))
+ .clipShape(RoundedCorner(radius: 16, corners: [.topLeft, .topRight, .bottomRight]))
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 24)
@@ -83,27 +66,14 @@ private struct BreadGuideView: View {
// MARK: (F)talkBubble
@ViewBuilder
private func talkBubble() -> some View {
- VStack(alignment: .trailing, spacing: 5) {
- Text(
- hasAvailableSummaryRecord ? "빵식이가 요약할 수 있는 책이 있어요!"
- : "셰프님의 독서 기록을 기다리고 있어요"
- )
- .foregroundStyle(.gray9)
- .brStyleFont(.pretendard(.semiBold, size: 13), lineHeight: 1.3, letterSpacing: 0.02)
-
- HStack(spacing: 3) {
- Text("자세히 보기")
- .brStyleFont(.pretendard(.regular, size: 10), lineHeight: 1.3, letterSpacing: 0.02)
- Image(systemName: SFSymbol.chevronRight.name)
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: 7, height: 7)
- }.foregroundStyle(.gray5)
- }
- .padding(.vertical, 8)
- .padding(.horizontal, 16)
- .background(.brown4.opacity(0.4))
- .clipShape(RoundedCorner(radius: 16, corners: [.topLeft, .topRight, .bottomRight]))
+ HStack(spacing: 3) {
+ Text("자세히 보기")
+ .brStyleFont(.pretendard(.regular, size: 10), lineHeight: 1.3, letterSpacing: 0.02)
+ Image(systemName: SFSymbol.chevronRight.name)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 7, height: 7)
+ }.foregroundStyle(.gray5)
}
}
@@ -223,7 +193,7 @@ private struct RecommandSectionView: View {
switch bookList.state {
case .loading:
- LoadingView()
+ BouncingImageLoadingView()
.frame(height: 140)
case .failed(let error):
@@ -248,7 +218,6 @@ private struct RecommandSectionView: View {
#Preview {
PreviewableContainer {
- HomeView(selectedTab: .constant(.home))
- .environmentObject(Coordinator())
+ HomeView()
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Home/HomeViewModel.swift b/B.READ/B.READ/Sources/Presentation/Home/HomeViewModel.swift
index a6ef6741..9dd220ac 100644
--- a/B.READ/B.READ/Sources/Presentation/Home/HomeViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Home/HomeViewModel.swift
@@ -10,50 +10,46 @@ import Foundation
final class HomeViewModel: ObservableObject {
// MARK: - State
- @Published var availableSummaryRecordId: String? = nil
@Published var recentRecords: [RecordCellVO] = []
@Published var bestSellerList: [BestSellerListVO] = []
var currentTask: Task? = nil
- private var selectedCategories: [Category] = []
-
// MARK: - Dependency
@Dependency private var libraryUseCase: LibraryUseCase
@Dependency private var recommandUseCase: RecommandUseCase
@Dependency private var profileUseCase: ProfileUseCase
+ init() {
+ //print("HomeViewModel 생성")
+ }
+
// MARK: - Action
enum Action {
case onAppear
+ case fetchBestSeller
case cancelTask
}
func send(_ action: Action) {
switch action {
case .onAppear:
- fetchAvailableSummaryRecord()
fetchRecentRecords()
fetchCategories()
+ case .fetchBestSeller:
+ fetchCategories()
case .cancelTask:
currentTask?.cancel()
}
}
+
+ deinit {
+ // print("HomeViewModel 소멸")
+ }
}
// MARK: - Internal Function
private extension HomeViewModel {
- func fetchAvailableSummaryRecord() {
- Task {
- do {
- let record = try await libraryUseCase.loadRecentRecordAvailableForSummary()
- await MainActor.run { self.availableSummaryRecordId = record.id }
- } catch RepositoryError.dataNotFound {
- await MainActor.run { self.availableSummaryRecordId = nil }
- }
- }
- }
-
func fetchRecentRecords() {
Task {
let records = try await libraryUseCase.loadRecentUpdatedReadingRecord(maxCount: 3)
@@ -70,9 +66,6 @@ private extension HomeViewModel {
do {
let data = try await profileUseCase.fetchUserInfo()
- if self.selectedCategories == data.categories { return }
- self.selectedCategories = data.categories
-
let lists: [BestSellerListVO] = data.categories.compactMap { category in
guard let categoryType = CategoryType(rawValue: category.id) else { return nil }
return BestSellerListVO(category: categoryType, bestSellers: [], state: .loading)
@@ -89,7 +82,7 @@ private extension HomeViewModel {
}
}
}
-
+
func fetchBestSellers(for categories: [CategoryType]) async {
let results: [BestSellerListVO?] = await withTaskGroup(of: BestSellerListVO?.self) { group in
for category in categories {
@@ -110,13 +103,15 @@ private extension HomeViewModel {
}
}
}
-
+
return await group.reduce(into: [BestSellerListVO?]()) { $0.append($1) }
}
-
+
await MainActor.run {
self.bestSellerList = results.compactMap { $0 }
}
+
}
+
}
diff --git a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridCell.swift b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridCell.swift
index ba7da5b2..14ffecb2 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridCell.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridCell.swift
@@ -60,7 +60,7 @@ struct LibraryGridCell: View {
.resizable()
.aspectRatio(contentMode: .fill)
} else {
- Image(.exampleCover)
+ Image(.exampleBook)
.resizable()
.aspectRatio(contentMode: .fill)
}
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..88a78ae6 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryGridView.swift
@@ -30,7 +30,6 @@ struct LibraryGridView: View {
}
}
} // : LazyVGrid
- .animation(.easeInOut(duration: 0.5), value: records)
} // : ScrollView
.scrollIndicators(.hidden)
.padding(.top, 8)
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..539560f4 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListCell.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListCell.swift
@@ -75,7 +75,7 @@ struct LibraryListCell: View {
.resizable()
.aspectRatio(contentMode: .fill)
} else {
- Image(.exampleCover)
+ Image(.exampleBook)
.resizable()
.aspectRatio(contentMode: .fill)
}
diff --git a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListView.swift b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListView.swift
index 943016a0..39fe8c04 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryListView.swift
@@ -23,8 +23,7 @@ struct LibraryListView: View {
coordinator.push(.libraryDetail(id: record.id))
}
}
- } // : LazyVStack
- .animation(.easeInOut(duration: 0.5), value: records)
+ }
} // : ScrollView
.scrollIndicators(.hidden)
.padding(.top, 8)
diff --git a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryView.swift b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryView.swift
index 0cdca09f..48f96e9d 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/LibraryView/LibraryView.swift
@@ -31,18 +31,13 @@ struct LibraryView: View {
ZStack(alignment: .topTrailing) {
VStack(alignment: .trailing, spacing: 0) {
// 상단 탭바
- ScrollViewReader { proxy in
- ScrollView(.horizontal, showsIndicators: false) {
- TopTabBar(tabs: viewModel.tabs, selectedIndex: $viewModel.selectedTab)
- .frame(width: 450, height: 34)
- .onChange(of: viewModel.selectedTab) { _, newValue in
- viewModel.send(.selectTab)
- withAnimation {
- proxy.scrollTo(newValue, anchor: newValue == 0 ? .leading : .trailing)
- }
- }
- } // : ScrollView
- } // : ScrollViewReader
+ ScrollView(.horizontal, showsIndicators: false) {
+ TopTabBar(tabs: viewModel.tabs, selectedIndex: $viewModel.selectedTab)
+ .frame(width: 450, height: 34)
+ .onChange(of: viewModel.selectedTab) {
+ viewModel.send(.selectTab)
+ }
+ } // : ScrollView
HStack(spacing: 8) {
// 정렬 버튼
@@ -65,13 +60,11 @@ struct LibraryView: View {
.foregroundStyle(.gray2)
.padding(.top, layoutPadding)
- if viewModel.viewState == .loading {
- LoadingView()
- } else if viewModel.displayRecords.isEmpty {
- FailedView(
- title: "😢 독서 기록을 작성하러 가볼까요?",
- desp: "카테고리에 일치하는 독서 기록이 없습니다."
- )
+ // 독서기록 목록 뷰
+ if viewModel.displayRecords.isEmpty {
+ Text("독서 기록이 없습니다.")
+ .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1.0)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
} else if displayMode == .list {
LibraryListView(records: $viewModel.displayRecords)
} else {
@@ -80,25 +73,9 @@ struct LibraryView: View {
} // : VStack - 책빵 화면
.padding(.top, layoutPadding)
.padding(.horizontal, 24)
- .animation(.easeInOut(duration: 0.5), value: displayMode)
} // : ZStack
.background(.backgroundDefault)
- .gesture(
- DragGesture().onEnded { value in
- let distance: CGFloat = 50 // 얼마나 이동하면 인식할지
-
- if value.translation.width < -distance { // 오른쪽 → 왼쪽 (다음 탭)
- if viewModel.selectedTab < 4 {
- viewModel.selectedTab += 1
- }
- } else if value.translation.width > distance { // 왼쪽 → 오른쪽 (이전 탭)
- if viewModel.selectedTab > 0 {
- viewModel.selectedTab -= 1
- }
- }
- }
- ) // : gesture - 제스처로 탭이동
.onAppear {
print("appear 작동")
viewModel.send(.onAppear)
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..870c9a73 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordBookSection.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordBookSection.swift
@@ -20,7 +20,7 @@ struct RecordBookSection: View {
.aspectRatio(contentMode: .fill)
} else {
// TODO: - [시르] 사진이 없을때, 들어갈 이미지 or 도형 추가
- Image(.exampleCover)
+ Image(.exampleBook)
.resizable()
.aspectRatio(contentMode: .fill)
}
diff --git a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordDetailView.swift b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordDetailView.swift
index f56de003..20f7c133 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordDetailView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordDetailView.swift
@@ -30,14 +30,11 @@ struct RecordDetailView: View {
RecordBookSection(record: $viewModel.record)
- RecordStatsSection(viewModel: viewModel)
+ RecordStatsSection(record: $viewModel.record)
.padding(.top, 8)
TopTabBar(
- tabs: [
- TabItem(title: "메모", image: Image(.menuBread)),
- TabItem(title: "문장", image: Image(.donut))
- ],
+ tabs: [TabItem(title: "메모"), TabItem(title: "문장")],
selectedIndex: $viewModel.selectedTab
)
.frame(height: 34)
@@ -80,15 +77,7 @@ struct RecordDetailView: View {
print("DetailView OnAppear")
viewModel.send(.onAppear)
if showAddMenu { UINavigationBar.showOverlay(duration: 0.0) }
- else { UINavigationBar.removeOverlay(duration: 0.0) }
} // : onAppear
- .onChange(of: coordinator.paths) {
- if let id = viewModel.record?.id,
- coordinator.paths.last == .libraryDetail(id: id)
- {
- viewModel.send(.onAppear)
- }
- }
.onChange(of: needRefresh) { oldValue, newValue in
if newValue {
viewModel.send(.onAppear)
@@ -127,15 +116,18 @@ struct RecordDetailView: View {
// MARK: - (F)topBarTrailingButton
private func topBarTrailingButton() -> some View {
- HStack(spacing: 4) {
+ HStack(spacing: 0) {
Button {
viewModel.send(.onTapFavorite)
} label: {
if let isFavorite = viewModel.record?.isFavorite {
- Image(systemName: isFavorite ? SFSymbol.bookMarkFill.name : SFSymbol.bookMark.name)
+ Image(systemName: isFavorite ? "bookmark.fill" : "bookmark")
+ .resizable()
+ .frame(width: 12, height: 24)
+ } else {
+ Image(systemName: "bookmark")
.resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: 22, height: 22)
+ .frame(width: 12, height: 24)
}
}
Button {
@@ -185,7 +177,6 @@ private struct AddActionView: View {
Button {
guard let record = viewModel.record else { return }
UINavigationBar.removeOverlay(duration: 0.0)
- self.showAddMenu = false
coordinator.push(.memo(record: record))
} label: {
Text("메모 작성")
@@ -194,7 +185,6 @@ private struct AddActionView: View {
Button {
guard let record = viewModel.record else { return }
UINavigationBar.removeOverlay(duration: 0.0)
- self.showAddMenu = false
coordinator.push(.sentenceInput(mode: .create(record: record)))
} label: {
Text("문장 수집")
@@ -230,8 +220,8 @@ private struct AddActionView: View {
.shadow(color: .black.opacity(0.25), radius: 4, y: 4)
)
}
- .padding(.trailing, 24)
- .padding(.bottom, 24)
+ .padding(.trailing, 32)
+ .padding(.bottom, 28)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordNotesSection.swift b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordNotesSection.swift
index 1aa088ce..b572c93d 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordNotesSection.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordNotesSection.swift
@@ -59,12 +59,9 @@ struct RecordNotesSection: View {
} // : ForEach
}
} // : LazyVStcks
- .animation(.easeInOut(duration: 0.5), value: viewModel.memos)
- .animation(.easeInOut(duration: 0.5), value: viewModel.quotes)
- .animation(.easeInOut(duration: 0.3), value: viewModel.selectedTab)
.frame(maxWidth: .infinity)
.padding(.horizontal, 8)
- .padding(.bottom, 100)
+ .padding(.bottom, 72)
.confirmationDialog(
"메뉴를 선택하세요",
isPresented: $showMenuActionSheet,
diff --git a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordStatsSection.swift b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordStatsSection.swift
index c3e5e0e8..7b2edad9 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordStatsSection.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/RecordView/RecordStatsSection.swift
@@ -9,73 +9,33 @@ import SwiftUI
// MARK: - (S)RecordStatsSection
struct RecordStatsSection: View {
- @ObservedObject var viewModel: RecordDetailViewModel
- @EnvironmentObject private var coordinator: Coordinator
+ @Binding var record: RecordDetailVO?
- private let contentHeaderFontSize: CGFloat = 16
private let contentFontSize: CGFloat = 14
private let layoutPadding: CGFloat = 8
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// 기대 지수, 평점
- if viewModel.record?.readingState == .notStart {
+ if record?.readingState == .notStart {
VStack(alignment: .leading, spacing: 8) {
Text("기대지수")
- ScoreBoardView(viewModel.record?.heart ?? 0, type: .heart)
+ ScoreBoardView(record?.heart ?? 0, type: .heart)
}
- .brStyleFont(.pretendard(.semiBold, size: contentHeaderFontSize), lineHeight: 0.95)
- } else if let record = viewModel.record, record.readingState == .finished {
- HStack {
- VStack(alignment: .leading, spacing: 8) {
- Text("평점")
- ScoreBoardView(record.star, type: .star)
- } // : VStack
- .brStyleFont(.pretendard(.semiBold, size: contentHeaderFontSize), lineHeight: 0.95)
- .frame(maxWidth: .infinity, alignment: .leading)
-
-
- if !viewModel.memos.isEmpty {
- Button {
- if let summaryData = viewModel.summary {
- coordinator.push(
- .summaryDetail(
- id: summaryData.id,
- record: viewModel.record!,
- memos: viewModel.memos,
- quotes: viewModel.quotes
- )
- )
- } else {
- coordinator
- .push(
- .createSummary(
- record: viewModel.record!,
- memos: viewModel.memos,
- quotes: viewModel.quotes
- )
- )
- }
- } label: {
- Image(.breadButton)
- .resizable()
- .aspectRatio(contentMode: .fit)
- }
- .frame(width: 48, height: 48)
- .frame(maxWidth: .infinity, alignment: .trailing)
- .padding(.trailing, 16)
- }
- } // : HStack
- .frame(maxWidth: .infinity)
-
- recordReview()
+ .brStyleFont(.pretendard(.semiBold, size: 16), lineHeight: 0.95)
+ } else if record?.readingState == .finished {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("평점")
+ ScoreBoardView(record?.star ?? 0, type: .star)
+ }
+ .brStyleFont(.pretendard(.semiBold, size: 16), lineHeight: 0.95)
}
// 독서 기간
VStack(alignment: .leading, spacing: layoutPadding) {
Text("독서 기간")
.brStyleFont(
- .pretendard(.semiBold, size: contentHeaderFontSize),
+ .pretendard(.semiBold, size: contentFontSize),
lineHeight: 0.95
)
@@ -86,9 +46,9 @@ struct RecordStatsSection: View {
} // : VStack
// 독서 진행률 프로그래스바
- if viewModel.record?.readingState != .finished,
- let currentPage = viewModel.record?.currentPage,
- let totalPage = viewModel.record?.totalPage
+ if record?.readingState != .finished,
+ let currentPage = record?.currentPage,
+ let totalPage = record?.totalPage
{
PageProgressbar(currentPage: currentPage, totalPage: totalPage)
.frame(height: 28)
@@ -100,7 +60,7 @@ struct RecordStatsSection: View {
@ViewBuilder
private func recordPeriodView() -> some View {
HStack(spacing: layoutPadding) {
- if let period = viewModel.record?.period, let startDate = period.startDate {
+ if let period = record?.period, let startDate = period.startDate {
HStack(spacing: layoutPadding) {
Text("시작")
.brStyleFont(
@@ -146,38 +106,15 @@ struct RecordStatsSection: View {
.padding(.vertical, layoutPadding)
.padding(.horizontal, 16)
}
-
- // MARK: - (F)recordReview
- private func recordReview() -> some View {
- VStack(alignment: .leading, spacing: 8) {
- Text("한줄평")
- .brStyleFont(.pretendard(.semiBold, size: contentHeaderFontSize), lineHeight: 0.95)
-
- ZStack(alignment: .topLeading) {
- RoundedRectangle(cornerRadius: 8)
- .fill(.gray0)
-
- if let review = viewModel.record?.review, !review.isEmpty {
- Text(review)
- .brStyleFont(.pretendard(.regular, size: contentFontSize), lineHeight: 1.3)
- .padding(16)
- } else {
- Text("짧은 감상평을 남겨보세요")
- .brStyleFont(.pretendard(.regular, size: contentFontSize), lineHeight: 1.3)
- .foregroundStyle(.gray5)
- .padding(16)
- }
- } // : ZStack
- } // : VStack
- }
}
#Preview {
- let recordID = DummyData.dummyRecords[2].id
+ @Previewable @State var record: RecordDetailVO? = RecordDetailVO(
+ record: DummyData.dummyRecords[1],
+ book: DummyData.dummyBooks[1]
+ )
+
PreviewableContainer {
- NavigationStack {
- RecordDetailView(viewModel: .init(recordID: recordID))
- .environmentObject(Coordinator())
- }
+ RecordStatsSection(record: $record)
}
}
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..65158140 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/ViewModel/LibraryViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/ViewModel/LibraryViewModel.swift
@@ -11,12 +11,8 @@ import SwiftUI
// MARK: - (C)LibraryViewModel
final class LibraryViewModel: ObservableObject {
- enum ViewState {
- case loading
- case loaded
- }
-
// MARK: - State
+ // 탭바
@Published var tabs: [TabItem] = [
TabItem(title: "전체(0)"),
TabItem(title: "읽은 책(0)"),
@@ -30,12 +26,11 @@ final class LibraryViewModel: ObservableObject {
@Published var selectedSort: [SortOption] = [.recent, .recent, .recent, .recent, .recent]
// 뷰로 보여주는 독서 기록
@Published var displayRecords: [RecordCellVO] = []
- @Published var viewState: ViewState = .loading
+
// MARK: - Internal Variable
// DB에서 가져온 전체 독서기록
private var records: [RecordCellVO] = []
- private var filteredRecords: [RecordCellVO] = []
// MARK: - Dependency
@Dependency
@@ -52,75 +47,58 @@ final class LibraryViewModel: ObservableObject {
func send(_ action: Action) {
switch action {
case .onAppear:
- self.loadRecords()
+ Task { [weak self] in
+ guard let self = self else { return }
+ //1. 전체 독서 기록을 불러옴
+ await self.loadRecords()
+ await withTaskGroup(of: Void.self) { group in
+ group.addTask {
+ // 2. 불러온 독서 기록의 상태별 개수 확인
+ await self.loadTabs()
+ }
+
+ group.addTask {
+ // 3. 선택된 탭을 기준으로 필터 적용
+ await self.filterRecords()
+ // 4. 필터 적용된 독서 기록에 정렬 적용
+ await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab])
+ }
+ }
+ }
case .selectTab:
- self.selectTab()
+ Task { [weak self] in
+ guard let self = self else { return }
+ // 1. 선택된 탭을 기준으로 필터 적용
+ await self.filterRecords()
+ // 2. 필터 적용된 독서 기록에 정렬 적용
+ await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab])
+ }
case .selectSort:
- self.selectSort()
- }
- }
-}
-
-
-// MARK: - (F)LibraryViewModel - 액션 함수
-private extension LibraryViewModel {
- // 독서 기록을 불러옴
- func loadRecords() {
- viewState = .loading
- Task {
- await fetchRecords()
- await withTaskGroup(of: Void.self) { group in
- group.addTask {
- // 2. 불러온 독서 기록의 상태별 개수 확인
- await self.loadTabs()
- }
-
- group.addTask {
- // 3. 선택된 탭을 기준으로 필터 적용
- await self.filterRecords()
- // 4. 필터 적용된 독서 기록에 정렬 적용
- await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab])
- }
+ Task { [weak self] in
+ guard let self = self else { return }
+ // 1. 현재 독서 기록에 정렬 적용
+ await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab])
}
- await MainActor.run {
- viewState = .loaded
- }
- }
- }
-
- // 상단 탭바를 선택
- func selectTab() {
- Task {
- // 1. 선택된 탭을 기준으로 필터 적용
- await self.filterRecords()
- // 2. 필터 적용된 독서 기록에 정렬 적용
- await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab])
- }
- }
-
- // 정렬을 선택
- func selectSort() {
- Task {
- await self.sortDisplayRecords(by: self.selectedSort[self.selectedTab])
}
}
}
-// MARK: - (F)LibraryViewModel - 내부 함수(async)
+// MARK: - (F)LibraryViewModel
private extension LibraryViewModel {
+
/// 독서 기록 정보를 불러옴
- func fetchRecords() async {
+ func loadRecords() async {
do {
// 1. 독서 기록 패치
let infos: [(record: Record, book: Book)] = try await libraryUseCase.loadRecordList()
// 2. Entity -> VO
self.records = infos.map { RecordCellVO(record: $0.record, book: $0.book) }
} catch {
+ // TODO: - [시르] 에러 메시지를 띄우는 코드 추가
// 3. 패치하던중 오류 발생 시 배열은 빈 배열을 반환하고, 에러 메시지를 띄움
- print(error.localizedDescription)
self.records = []
}
}
@@ -172,14 +150,14 @@ private extension LibraryViewModel {
// 3. 필터 적용한 독서 기록을 뷰에 반영
await MainActor.run {
- self.filteredRecords = filterRecord
+ self.displayRecords = filterRecord
}
}
/// 정렬 기준에 따라서 displayRecords를 정렬
func sortDisplayRecords(by: SortOption = .recent) async {
// 1. 정렬한 결과
- let sortedRecords: [RecordCellVO] = filteredRecords.sorted(by: by.sort)
+ let sortedRecords: [RecordCellVO] = displayRecords.sorted(by: by.sort)
// 2. 결과를 뷰에 반영
await MainActor.run {
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..d0223a91 100644
--- a/B.READ/B.READ/Sources/Presentation/Library/ViewModel/RecordDetailViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Library/ViewModel/RecordDetailViewModel.swift
@@ -24,7 +24,6 @@ final class RecordDetailViewModel: ObservableObject {
private let recordID: String
var selectedQuote: QuoteVO? = nil
var selectedMemo: MemoVO? = nil
- var summary: SummaryVO? = nil
init(recordID: String) {
self.recordID = recordID
@@ -90,10 +89,7 @@ private extension RecordDetailViewModel {
// 4. 문장 VO 생성
self.quotes = info.record.quotes
.map { QuoteVO($0, record: RecordDetailVO(record: info.record, book: info.book)) }
-
- if let summaryData = info.record.summary {
- self.summary = SummaryVO(summaryData)
- }
+ // TODO: - [시르] 서머리 VO 정의 후 생성 해야함
}
await withTaskGroup(of: Void.self) { group in
diff --git a/B.READ/B.READ/Sources/Presentation/Login/LoginView.swift b/B.READ/B.READ/Sources/Presentation/Login/LoginView.swift
index 0bf43d39..d877a387 100644
--- a/B.READ/B.READ/Sources/Presentation/Login/LoginView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Login/LoginView.swift
@@ -23,11 +23,8 @@ struct LoginView: View {
.brStyleFont(.pretendard(.light, size: 18), lineHeight: 1.1)
.padding(.top, 60)
-
- Image(.splashLogo)
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(height: 60)
+ Text("B. READ")
+ .brStyleFont(.peaceSans(size: 48), lineHeight: 1.1)
.padding(.top, 16)
Text("누르면 초기 설정을 시작해요!")
@@ -42,7 +39,6 @@ struct LoginView: View {
}
.padding(.horizontal, 56)
.frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(.backgroundDefault)
}
diff --git a/B.READ/B.READ/Sources/Presentation/Memo/MemoView.swift b/B.READ/B.READ/Sources/Presentation/Memo/MemoView.swift
index 90e1ac23..277814bb 100644
--- a/B.READ/B.READ/Sources/Presentation/Memo/MemoView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Memo/MemoView.swift
@@ -27,67 +27,56 @@ struct MemoView: View {
}
var body: some View {
- ScrollViewReader { proxy in
- ScrollView {
- VStack(alignment: .leading, spacing: 24) {
-
- GuideSectionView(viewModel: viewModel, showGuideAlert: $showGuideAlert)
-
- pageSection()
-
- memoSection()
-
- }
- .id("bottom")
- .navigationTitle(viewModel.createAt.string(format: .dotSeparatedFull))
- .frame(maxHeight: .infinity, alignment: .top)
- .padding(.horizontal, 24)
- .animation(.easeOut(duration: 0.25), value: viewModel.guideStatus)
- .onChange(of: viewModel.isSaveComplete) {
- coordinator.pop()
- }
- .onChange(of: textEditorFocused) {
- if textEditorFocused {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
- withAnimation {
- proxy.scrollTo("bottom", anchor: .bottom)
- }
- }
- }
- }
- .toolbar {
- ToolbarItem(placement: .topBarTrailing) {
- Button {
- if let start = Int(viewModel.startPage),
- let end = Int(viewModel.endPage), start <= end, end <= totalPage {
- viewModel.send(.saveMemo)
- } else {
- showErrorAlert = true
- }
- } label: {
- Text("저장")
- .brStyleFont(.pretendard(.regular, size: 16), lineHeight: 1.0)
- .foregroundStyle(.green6)
- .opacity(isButtonEnabled ? 1 : 0)
- .disabled(!isButtonEnabled)
- .animation(.easeInOut(duration: 0.2), value: isButtonEnabled)
+ ScrollView {
+ VStack(alignment: .leading, spacing: 24) {
+
+ GuideSectionView(viewModel: viewModel, showGuideAlert: $showGuideAlert)
+
+ pageSection()
+
+ memoSection()
+
+ }
+ .id("bottom")
+ .navigationTitle(viewModel.createAt.string(format: .dotSeparatedFull))
+ .frame(maxHeight: .infinity, alignment: .top)
+ .padding(.horizontal, 24)
+ .animation(.easeOut(duration: 0.25), value: viewModel.guideStatus)
+ .onChange(of: viewModel.isSaveComplete) {
+ coordinator.pop()
+ }
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ if let start = Int(viewModel.startPage),
+ let end = Int(viewModel.endPage), start <= end, end <= totalPage {
+ viewModel.send(.saveMemo)
+ } else {
+ showErrorAlert = true
}
+ } label: {
+ Text("저장")
+ .brStyleFont(.pretendard(.regular, size: 16), lineHeight: 1.0)
+ .foregroundStyle(.green6)
+ .opacity(isButtonEnabled ? 1 : 0)
+ .disabled(!isButtonEnabled)
+ .animation(.easeInOut(duration: 0.2), value: isButtonEnabled)
}
}
- .alert("가이드를 삭제하시겠습니까?", isPresented: $showGuideAlert) {
- Button("취소", role: .cancel) { }
- Button("삭제", role: .destructive) { viewModel.send(.deleteGuides) }
- } message: {
- Text("빵식이의 가이드를 삭제하시겠습니까?\n(다시 생성되지 않습니다)")
- }
- .alert("저장 실패", isPresented: $showErrorAlert){
- Button("확인", role: .cancel) { }
- } message: {
- Text("올바른 페이지 번호가 아닙니다.")
- }
}
- .background(.backgroundDefault)
+ .alert("가이드를 삭제하시겠습니까?", isPresented: $showGuideAlert) {
+ Button("취소", role: .cancel) { }
+ Button("삭제", role: .destructive) { viewModel.send(.deleteGuides) }
+ } message: {
+ Text("빵식이의 가이드를 삭제하시겠습니까?\n(다시 생성되지 않습니다)")
+ }
+ .alert("저장 실패", isPresented: $showErrorAlert){
+ Button("확인", role: .destructive) { }
+ } message: {
+ Text("올바른 페이지 번호가 아닙니다.")
+ }
}
+ .background(.backgroundDefault)
}
// 숫자 외에 텍스트 필터링 및 0 못오게
private func formatDigits(_ input: String) -> String {
@@ -198,7 +187,7 @@ private struct GuideSectionView: View {
private func guideText() -> some View {
switch viewModel.guideStatus {
case .loading:
- LoadingView(text: "빵식이에게 가이드 요청중..")
+ ProgressView()
case .empty:
Image(.happyBread)
diff --git a/B.READ/B.READ/Sources/Presentation/Memo/MemoViewModel.swift b/B.READ/B.READ/Sources/Presentation/Memo/MemoViewModel.swift
index 64265d64..8554820d 100644
--- a/B.READ/B.READ/Sources/Presentation/Memo/MemoViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Memo/MemoViewModel.swift
@@ -72,7 +72,8 @@ final class MemoViewModel: ObservableObject {
// MARK: - Internal Function
private extension MemoViewModel {
func fetchMemo(id: String) {
- Task {
+ Task { [weak self] in
+ guard let self else { return }
do {
let memo = try await memoUseCase.fetchMemo(id: id)
self.memo = memo
@@ -91,7 +92,8 @@ private extension MemoViewModel {
}
func saveMemo() {
- Task {
+ Task { [weak self] in
+ guard let self else { return }
memo?.content = self.content
memo?.pages = (Int(self.startPage)!, Int(self.endPage)!)
memo?.guides = self.guides.map { Guide(date: .now, content: $0) }
@@ -106,7 +108,9 @@ private extension MemoViewModel {
}
func generateGuides() {
- Task {
+ Task { [weak self] in
+ guard let self else { return }
+
await MainActor.run { self.guideStatus = .loading }
do {
diff --git a/B.READ/B.READ/Sources/Presentation/MyPage/MyPageView.swift b/B.READ/B.READ/Sources/Presentation/MyPage/MyPageView.swift
index 45e5186f..7bea6bca 100644
--- a/B.READ/B.READ/Sources/Presentation/MyPage/MyPageView.swift
+++ b/B.READ/B.READ/Sources/Presentation/MyPage/MyPageView.swift
@@ -13,17 +13,14 @@ struct MyPageView: View {
@StateObject private var viewModel = SettingViewModel()
var body: some View {
- VStack(alignment: .leading) {
+ VStack(alignment: .leading, spacing: 32) {
+
+ nicknameButton()
+
+ MenuListView(coordinator: coordinator, viewModel: viewModel)
- LogoView()
- VStack {
- nicknameButton()
-
- MenuListView(coordinator: coordinator, viewModel: viewModel)
- .padding(.top, 32)
- }
- .padding(.horizontal, 24)
}
+ .padding(.horizontal, 24)
.frame(maxHeight: .infinity, alignment: .top)
.background(.backgroundDefault)
.onAppear {
@@ -113,6 +110,15 @@ private struct MenuListView: View {
// TODO: Sprint 3
//menuTitle(title: "SNS 인증")
+ Button {
+ print("초기화")
+ } label: {
+ Text("초기화")
+ .brStyleFont(.pretendard(.light, size: 18), lineHeight: 1.35, letterSpacing: 0.02)
+ .foregroundStyle(.red)
+ .underline()
+ }.padding(.top, menuSpacing)
+
Image(.readBreadMyPage)
.resizable()
.aspectRatio(contentMode: .fit)
@@ -179,8 +185,6 @@ private struct MenuListView: View {
#Preview {
- PreviewableContainer {
- MyPageView()
- .environmentObject(Coordinator())
- }
+ MyPageView()
+ .environmentObject(Coordinator())
}
diff --git a/B.READ/B.READ/Sources/Presentation/OnBoarding/LaunchScreen.swift b/B.READ/B.READ/Sources/Presentation/OnBoarding/LaunchScreen.swift
deleted file mode 100644
index 79484884..00000000
--- a/B.READ/B.READ/Sources/Presentation/OnBoarding/LaunchScreen.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-//
-// LaunchScreen.swift
-// B.READ
-//
-// Created by 김도연 on 6/7/25.
-//
-
-import SwiftUI
-
-struct LaunchScreen: View {
- @State private var scale: CGFloat = 1.0
-
- var body: some View {
- VStack(spacing: 30) {
- Image(.splash)
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: 80, height: 80)
- .scaleEffect(scale)
- .onAppear {
- animatePulse()
- }
-
- Image(.splashLogo)
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: 180)
- .padding(.bottom, 100)
- }
- }
-
- private func animatePulse() {
- let baseScale: CGFloat = 1.0
- let peakScale: CGFloat = 1.15
- let duration: TimeInterval = 0.3
- let pause: TimeInterval = 2.0
-
- func pulseOnce() {
- withAnimation(.interpolatingSpring(stiffness: 200, damping: 5)) {
- scale = peakScale
- }
-
- DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
- withAnimation(.interpolatingSpring(stiffness: 200, damping: 5)) {
- scale = baseScale
- }
-
- DispatchQueue.main.asyncAfter(deadline: .now() + duration + pause) {
- pulseOnce()
- }
- }
- }
-
- pulseOnce()
- }
-}
-
-#Preview {
- LaunchScreen()
-}
diff --git a/B.READ/B.READ/Sources/Presentation/OnBoarding/SplashView.swift b/B.READ/B.READ/Sources/Presentation/OnBoarding/SplashView.swift
new file mode 100644
index 00000000..8f2586dc
--- /dev/null
+++ b/B.READ/B.READ/Sources/Presentation/OnBoarding/SplashView.swift
@@ -0,0 +1,36 @@
+//
+// SplashView.swift
+// B.READ
+//
+// Created by 김도연 on 6/7/25.
+//
+
+import SwiftUI
+
+struct SplashView: View {
+ @State private var bounce = false
+
+ var body: some View {
+ VStack(spacing: 0) {
+// Image(.splash)
+// .resizable()
+// .aspectRatio(contentMode: .fit)
+// .frame(width: 100, height: 100)
+// .offset(y: bounce ? -20 : 10)
+// .animation(.easeInOut(duration: 0.5).repeatForever(autoreverses: true), value: bounce)
+// .onAppear {
+// bounce.toggle()
+// }
+
+ Image(.splashLogo)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 240)
+
+ }
+ }
+}
+
+#Preview {
+ SplashView()
+}
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/AlanSummaryView.swift b/B.READ/B.READ/Sources/Presentation/Record/View/AlanSummaryView.swift
deleted file mode 100644
index 730fb40e..00000000
--- a/B.READ/B.READ/Sources/Presentation/Record/View/AlanSummaryView.swift
+++ /dev/null
@@ -1,94 +0,0 @@
-//
-// AlanSummaryView.swift
-// B.READ
-//
-// Created by 김도연 on 6/1/25.
-//
-
-import SwiftUI
-
-// MARK: - (S)AlanSummaryView
-struct AlanSummaryView: View {
- @StateObject var viewModel: SummaryViewModel
- @EnvironmentObject var coordinator: Coordinator
-
- init(viewModel: @autoclosure @escaping () -> SummaryViewModel) {
- self._viewModel = StateObject(wrappedValue: viewModel())
- }
-
- var body: some View {
- Group {
- switch viewModel.dataState {
- case .loading:
- LoadingView(text: "요약노트 작성 중...")
- case .loaded:
- ScrollView {
- VStack(alignment: .center, spacing: 12) {
- if let startDate = viewModel.record.period.0,
- let endDate = viewModel.record.period.1 {
-
- let start = startDate.string(format: .dotSeparated)
- let end = endDate.string(format: .dotSeparated)
- InfoView(title: "🗓️ 독서 기간", content: "\(start) ~ \(end)")
- }
-
- if let summary = viewModel.summary {
- tagList(title: "🏷️ 감정 태그", tags: summary.tags)
- InfoView(title: "📚 요약", content: summary.content)
- }
-
- MultiInfoView(title: "🍞 문장", content: viewModel.quoteData)
-
- MultiInfoView(title: "📝 메모", content: viewModel.memoData)
- }
- }
- case .failed:
- FailedView(desp: "빵식이가 요약노트를 생성할 수 없습니다.")
- }
- }
- .navigationTitle(viewModel.record.title)
- .background(.backgroundDefault, ignoresSafeAreaEdges: .all)
- }
-
- // MARK: (F)tagList
- @ViewBuilder
- private func tagList(title: String, tags: [TagVO]) -> some View {
- let layoutPadding : CGFloat = 16
- let horizontalPadding : CGFloat = 24
-
- VStack(alignment: .leading, spacing: layoutPadding) {
- Text(title)
- .brStyleFont(.pretendard(.semiBold, size: 16), lineHeight: 1.2, letterSpacing: 0.02)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- ScrollView(.horizontal) {
- HStack(spacing: 12) {
- ForEach(tags, id: \.self) {
- Text($0.content)
- .foregroundStyle(.gray7)
- .brStyleFont(.pretendard(.medium, size: 12), lineHeight: 1.0, letterSpacing: -0.025)
- .padding(.horizontal, layoutPadding)
- .padding(.vertical, horizontalPadding/2)
- .background(.gray2.opacity(0.2))
- .clipShape(Capsule())
- }
- }
- }
- }
- .padding(.horizontal, horizontalPadding)
- .padding(.vertical, layoutPadding)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(.white)
- }
-}
-
-#Preview {
- let recordDetail = RecordDetailVO(record: DummyData.dummyRecords[1], book: DummyData.dummyBooks[1])
-
- AlanSummaryView(viewModel: .init(
- record: RecordDetailVO(record: DummyData.dummyRecords[1], book: DummyData.dummyBooks[1]),
- memos: DummyData.dummyMemos.map{ MemoVO($0, record: recordDetail) },
- quotes: DummyData.dummyQuote.map{ QuoteVO($0, record: recordDetail) })
- )
-
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/MemoListView.swift b/B.READ/B.READ/Sources/Presentation/Record/View/MemoListView.swift
index 395db99b..030a6dc2 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/View/MemoListView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/View/MemoListView.swift
@@ -23,7 +23,6 @@ struct MemoListView: View {
ForEach($group.memos) { $memo in
MemoCell(
content: memo.content,
- highlight: viewModel.highlightKeyword,
date: memo.createdAt,
startPage: memo.pages.0,
endPage: memo.pages.1
@@ -33,30 +32,17 @@ struct MemoListView: View {
}
.padding(.leading, 8)
}
-
+
} header: {
- Group {
- if let keyword = viewModel.highlightKeyword, !keyword.isEmpty {
- group.bookTitle.highlightedText(
- keyword: keyword,
- regularFont: Font(UIFont.pretendard(.semiBold, size: 18)),
- highlightFont: Font(UIFont.pretendard(.semiBold, size: 18))
- )
- }
- else {
- Text(group.bookTitle)
- }
- }
- .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1.0)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(.backgroundDefault)
- .padding(.top, 16)
+ Text(group.bookTitle)
+ .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1.0)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(.backgroundDefault)
+ .padding(.top, 16)
}// : Section
}
} // : LazyVStack
- .padding(.bottom, 40)
- .animation(.easeInOut(duration: 0.5), value: viewModel.displayMemoGroups)
} // : ScrollView
.background(.backgroundDefault)
.confirmationDialog(
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/NoteListCell.swift b/B.READ/B.READ/Sources/Presentation/Record/View/NoteListCell.swift
index 8f26d548..96120ef8 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/View/NoteListCell.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/View/NoteListCell.swift
@@ -51,7 +51,7 @@ struct NoteListCell: View {
.resizable()
.aspectRatio(contentMode: .fill)
} else {
- Image(.exampleCover)
+ Image(.exampleBook)
.resizable()
.aspectRatio(contentMode: .fill)
}
@@ -64,7 +64,7 @@ struct NoteListCell: View {
bookTitle: "싯타르타",
author: "헤르만헤세",
createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 4, day: 19))!,
- coverImage: Image(.exampleCover),
+ coverImage: Image(.exampleBook),
content: "테스트테스트테스트테스트테스트테스트",
recordId: "3"
)
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/NoteListview.swift b/B.READ/B.READ/Sources/Presentation/Record/View/NoteListview.swift
index f3ed553b..aeb8745a 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/View/NoteListview.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/View/NoteListview.swift
@@ -26,8 +26,6 @@ struct NoteListview: View {
}
} // : LazyVStack
- .padding(.bottom, 40)
- .animation(.easeInOut(duration: 0.5), value: viewModel.displayNotes)
}// : ScrollView
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/QuoteListView.swift b/B.READ/B.READ/Sources/Presentation/Record/View/QuoteListView.swift
index f3638ff4..732796d9 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/View/QuoteListView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/View/QuoteListView.swift
@@ -23,7 +23,6 @@ struct QuoteListView: View {
ForEach($group.quotes) { $quote in
QuoteCell(
content: quote.content,
- highlight: viewModel.highlightKeyword,
page: quote.page,
colorTone: ColorTone.tone(isbn: quote.isbn)
) {
@@ -33,27 +32,15 @@ struct QuoteListView: View {
.padding(.leading, 8)
}
} header: {
- Group {
- if let keyword = viewModel.highlightKeyword, !keyword.isEmpty {
- group.bookTitle.highlightedText(
- keyword: keyword,
- regularFont: Font(UIFont.pretendard(.semiBold, size: 18)),
- highlightFont: Font(UIFont.pretendard(.semiBold, size: 18))
- )
- } else {
- Text(group.bookTitle)
- }
- }
- .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1.0)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(.backgroundDefault)
- .padding(.top, 16)
+ Text(group.bookTitle)
+ .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1.0)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(.backgroundDefault)
+ .padding(.top, 16)
}// : Section
}
} // : LazyVStack
- .padding(.bottom, 40)
- .animation(.easeInOut(duration: 0.5), value: viewModel.displayQuoteGroups)
} // : ScrollView
.background(.backgroundDefault)
.confirmationDialog(
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/RecordMemoView.swift b/B.READ/B.READ/Sources/Presentation/Record/View/RecordMemoView.swift
index 3113aa08..afacf85c 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/View/RecordMemoView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/View/RecordMemoView.swift
@@ -17,9 +17,12 @@ struct RecordMemoView: View {
HStack {
SearchBar(
text: $viewModel.searchText,
- onSubmit: { viewModel.send(.onSubmit) },
- style: .compact
- )
+ onSubmit: {
+ if !viewModel.searchText.isEmpty {
+ viewModel.send(.onSubmit)
+ }
+ },
+ style: .compact)
SortMenu(
isOpened: $showSortMenu,
@@ -32,18 +35,9 @@ struct RecordMemoView: View {
}
} // : HStack
- if viewModel.memoGroups.isEmpty {
- FailedView(
- title: "😢 메모를 작성하러 가볼까요?",
- desp: "작성하신 메모가 없습니다."
- )
- } else if !viewModel.searchText.isEmpty && viewModel.displayMemoGroups.isEmpty {
- FailedView(desp: "\"\(viewModel.searchText)\"에 일치하는 검색 결과가 없습니다.")
- } else {
- MemoListView(viewModel: viewModel)
- .padding(.top, 8)
- .scrollIndicators(.never)
- }
+ MemoListView(viewModel: viewModel)
+ .padding(.top, 8)
+ .scrollIndicators(.never)
} // : VStack
.onAppear {
viewModel.send(.onAppear)
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/RecordNoteView.swift b/B.READ/B.READ/Sources/Presentation/Record/View/RecordNoteView.swift
index ccfe6e00..ce380e1b 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/View/RecordNoteView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/View/RecordNoteView.swift
@@ -17,9 +17,12 @@ struct RecordNoteView: View {
HStack {
SearchBar(
text: $viewModel.searchText,
- onSubmit: { viewModel.send(.onSubmit) },
- style: .compact
- )
+ onSubmit: {
+ if !viewModel.searchText.isEmpty {
+ viewModel.send(.onSubmit)
+ }
+ },
+ style: .compact)
SortMenu(
isOpened: $showSortMenu,
@@ -32,18 +35,9 @@ struct RecordNoteView: View {
}
} // : HStack
- if viewModel.notes.isEmpty {
- FailedView(
- title: "😢 완독 후, 다시 빵식이를 불러주세요.",
- desp: "요약을 진행한 독서 기록이 업습니다."
- )
- } else if !viewModel.searchText.isEmpty && viewModel.displayNotes.isEmpty {
- FailedView(desp: "\"\(viewModel.searchText)\"에 일치하는 검색 결과가 없습니다.")
- } else {
- NoteListview(viewModel: viewModel)
- .padding(.top, 8)
- .scrollIndicators(.never)
- }
+ NoteListview(viewModel: viewModel)
+ .padding(.top, 8)
+ .scrollIndicators(.never)
} // : VStack
.onAppear {
viewModel.send(.onAppear)
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/RecordQuoteView.swift b/B.READ/B.READ/Sources/Presentation/Record/View/RecordQuoteView.swift
index eb8cce0b..7c503301 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/View/RecordQuoteView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/View/RecordQuoteView.swift
@@ -17,9 +17,12 @@ struct RecordQuoteView: View {
HStack {
SearchBar(
text: $viewModel.searchText,
- onSubmit: { viewModel.send(.onSubmit) },
- style: .compact
- )
+ onSubmit: {
+ if !viewModel.searchText.isEmpty {
+ viewModel.send(.onSubmit)
+ }
+ },
+ style: .compact)
SortMenu(
isOpened: $showSortMenu,
@@ -31,19 +34,10 @@ struct RecordQuoteView: View {
viewModel.send(.selectSort)
}
} // : HStack
-
- if viewModel.quoteGroups.isEmpty {
- FailedView(
- title: "😢 문장을 작성하러 가볼까요?",
- desp: "작성하신 문장이 없습니다."
- )
- } else if !viewModel.searchText.isEmpty && viewModel.displayQuoteGroups.isEmpty {
- FailedView(desp: "\"\(viewModel.searchText)\"에 일치하는 검색 결과가 없습니다.")
- } else {
- QuoteListView(viewModel: viewModel)
- .padding(.top, 8)
- .scrollIndicators(.never)
- }
+
+ QuoteListView(viewModel: viewModel)
+ .padding(.top, 8)
+ .scrollIndicators(.never)
} // : VStack
.onAppear {
viewModel.send(.onAppear)
diff --git a/B.READ/B.READ/Sources/Presentation/Record/View/RecordView.swift b/B.READ/B.READ/Sources/Presentation/Record/View/RecordView.swift
index 75d483aa..79b6e3af 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/View/RecordView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/View/RecordView.swift
@@ -9,7 +9,7 @@ import SwiftUI
// MARK: - (S)RecordView
struct RecordView: View {
-
+
@State private var selectedTab: Int = 0
@StateObject private var memoViewModel = RecordMemoViewModel()
@StateObject private var quoteViewModel = RecordQuoteViewModel()
@@ -18,17 +18,13 @@ struct RecordView: View {
var body: some View {
VStack(spacing: .zero) {
TopTabBar(
- tabs: [
- TabItem(title: "메모", image: Image(.menuBread)),
- TabItem(title: "문장", image: Image(.donut)),
- TabItem(title: "빵식이", image: Image(.aiBread))
- ],
+ tabs: [TabItem(title: "메모"), TabItem(title: "문장"), TabItem(title: "빵식이")],
selectedIndex: $selectedTab
)
.frame(height: 34)
.padding(.top, 16)
- ZStack {
+ Group {
if selectedTab == 0 {
RecordMemoView(viewModel: memoViewModel)
} else if selectedTab == 1 {
@@ -36,25 +32,9 @@ struct RecordView: View {
} else if selectedTab == 2 {
RecordNoteView(viewModel: noteViewModel)
}
- }
- .animation(.easeInOut(duration: 0.5), value: selectedTab)
+ } // : Group
.padding(.top, 8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
- .gesture(
- DragGesture().onEnded { value in
- let distance: CGFloat = 50 // 얼마나 이동하면 인식할지
-
- if value.translation.width < -distance { // 오른쪽 → 왼쪽 (다음 탭)
- if selectedTab < 2 {
- selectedTab += 1
- }
- } else if value.translation.width > distance { // 왼쪽 → 오른쪽 (이전 탭)
- if selectedTab > 0 {
- selectedTab -= 1
- }
- }
- }
- ) // : gesture - 제스처로 탭이동
} // : VStack
.padding(.horizontal, 24)
.background(.backgroundDefault)
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..0bd91b50 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordMemoViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordMemoViewModel.swift
@@ -14,10 +14,9 @@ final class RecordMemoViewModel: ObservableObject {
@Published var displayMemoGroups: [MemoGroup] = []
@Published var searchText: String = ""
@Published var selectedSort: SortOption = .pageAscending
- @Published var highlightKeyword: String? = nil
// MARK: - Internal Variable
- private(set) var memoGroups: [MemoGroup] = []
+ private var memoGroups: [MemoGroup] = []
var selectedMemo: MemoVO? = nil
// MARK: - Dependency
@@ -41,7 +40,7 @@ final class RecordMemoViewModel: ObservableObject {
sortDisplayMemoGroups()
case .onSubmit:
- searchMemos()
+ print("검색어: \(searchText)")
case .deleteMemo(let id):
deleteMemo(id: id)
@@ -97,8 +96,8 @@ private extension RecordMemoViewModel {
await MainActor.run {
// 5. 만들어진 MemoGroup을 반영
self.memoGroups = memoGroups
- // 6. 검색어 필터를 진행
- searchMemos()
+ // 6. MemoGroup 정렬을 진행
+ sortDisplayMemoGroups()
}
} catch {
print("메모 로드 중 문제 발생")
@@ -108,7 +107,7 @@ private extension RecordMemoViewModel {
/// 보여주고자 하는 Memo의 순서를 정렬합니다.
func sortDisplayMemoGroups() {
- let sortedGroup = self.displayMemoGroups
+ let sortedGroup = memoGroups
// 1. 그룹 내부의 메모를 정렬
.map { group in
var sortedGroup = group
@@ -131,51 +130,4 @@ private extension RecordMemoViewModel {
loadMemoGroups()
}
}
-
- /// 검색어로 메모를 필터링 합니다.
- func searchMemos() {
- // 1. 검색어가 없으면 전체 메모를 보여줌(화이트스페이스, 줄바꿈 제거)
- let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmed.isEmpty else {
- self.searchText = ""
- self.highlightKeyword = nil
- self.displayMemoGroups = memoGroups
- self.sortDisplayMemoGroups()
- return
- }
-
- // 대소문자 구별하지 않음
- let keyword = trimmed.lowercased()
-
- // 2. 메모에서 검색어가 포함된 것을 필터링
- let filteredGroups = memoGroups.compactMap { group -> MemoGroup? in
- // 2-1. 책 제목을 필터링
- let bookTitleMatched = group.bookTitle.lowercased().contains(keyword)
-
- // 2-2. 메모 내용을 필터링
- let matchedMemos = group.memos.filter {
- $0.content.lowercased().contains(keyword)
- }
-
- // 2-3. 책 제목에 포함되면 전부, 내용만 포함되면 필터된 내용만
- if bookTitleMatched || !matchedMemos.isEmpty {
- return MemoGroup(
- isbn: group.isbn,
- bookTitle: group.bookTitle,
- memos: bookTitleMatched ? group.memos : matchedMemos
- )
- } else {
- return nil
- }
- }
-
- // 3. 필터한 내용을 저장
- self.displayMemoGroups = filteredGroups
-
- // 4. 필터한 내용을 정렬
- self.sortDisplayMemoGroups()
-
- // 5. 하이라이트 키워드 반영
- self.highlightKeyword = trimmed
- }
}
diff --git a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordNoteViewModel.swift b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordNoteViewModel.swift
index e929e10a..eb2978ef 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordNoteViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordNoteViewModel.swift
@@ -17,10 +17,10 @@ final class RecordNoteViewModel: ObservableObject {
@Published var selectedSort: SortOption = .recent
// MARK: - Internal Variable
- private(set) var notes: [NoteVO] = []
+ private var notes: [NoteVO] = []
// MARK: - Dependency
- // @Dependency private var noteUseCase: NoteUseCase
+// @Dependency private var noteUseCase: NoteUseCase
// MARK: - Action
enum Action {
@@ -38,7 +38,7 @@ final class RecordNoteViewModel: ObservableObject {
sortDisplayNotes()
case .onSubmit:
- searchNotes()
+ print("검색어: \(searchText)")
}
}
}
@@ -47,55 +47,27 @@ private extension RecordNoteViewModel {
/// 요약노트를 불러와서 뷰에 보여줄 형태로 가공합니다.
func loadSummarys() {
Task {
- // do {
- // // 1. 요약노트, 독서기록, 책정보를 튜플의 형태로 가져옵니다.
- // let infos: [(note: AlanSummary, record: Record, book: Book)] = try await noteUseCase.loadSummaryList()
- //
- // // 2. NoteVO 형태로 가공합니다. (Entity -> VO)
- // let allNotes: [NoteVO] = infos.map { NoteVO(record: $0.record, book: $0.book, note: $0.note) }
- // } catch {
- // // 패치 중 오류로 정보를 가져오지 못한 상태
- // self.notes = []
- // }
+// do {
+// // 1. 요약노트, 독서기록, 책정보를 튜플의 형태로 가져옵니다.
+// let infos: [(note: AlanSummary, record: Record, book: Book)] = try await noteUseCase.loadSummaryList()
+//
+// // 2. NoteVO 형태로 가공합니다. (Entity -> VO)
+// let allnotes: [NoteVO] = infos.map { NoteVO(record: $0.record, book: $0.book, note: $0.note) }
+// } catch {
+// // 패치 중 오류로 정보를 가져오지 못한 상태
+// self.notes = []
+// }
await MainActor.run {
-// setDummy()
- // 3. allNotes를 반영
- // self.notes = allNotes
- // 4. 검색어 필터를 진행
- searchNotes()
+ setDummy()
+ // 3. 선택된 정렬에 맞게 정렬을 진행
+ sortDisplayNotes()
}
} // : Task
}
- /// 보여주고자 하는 Note의 순서를 정렬합니다.
func sortDisplayNotes() {
- self.displayNotes = self.displayNotes.sorted(by: self.selectedSort.sort)
- }
-
- ///검색어로 노트를 필터링 합니다.
- func searchNotes() {
- // 1. 검색어가 없으면 전체 노트를 보여줌
- let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmed.isEmpty else {
- self.searchText = ""
- self.displayNotes = notes
- self.sortDisplayNotes()
- return
- }
-
- // 대소문자 구별하지 않음
- let keyword = searchText.lowercased()
-
- // 2. 책 제목으로 필터링을 진행
- let filteredNotes = notes.filter {
- $0.bookTitle.lowercased().contains(keyword)
- }
- // 3. 필터한 내용을 저장
- self.displayNotes = filteredNotes
-
- // 4. 필터한 내용을 정렬
- self.sortDisplayNotes()
+ self.displayNotes = self.notes.sorted(by: self.selectedSort.sort)
}
}
@@ -108,7 +80,7 @@ private extension RecordNoteViewModel {
bookTitle: "싯타르타",
author: "헤르만헤세",
createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 4, day: 19))!,
- coverImage: Image(.exampleCover),
+ coverImage: Image(.exampleBook),
content: "테스트테스트테스트테스트테스트테스트",
recordId: "3"
),
@@ -117,7 +89,7 @@ private extension RecordNoteViewModel {
bookTitle: "타이탄의 도구들",
author: "팀 페리스",
createdAt: Calendar.current.date(from: DateComponents(year: 2025, month: 5, day: 11))!,
- coverImage: Image(.exampleCover),
+ coverImage: Image(.exampleBook),
content: "트스테트스테트스테트스테트스테트스테트스테트스테트스테ㅍ",
recordId: "2"
)
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..613094a6 100644
--- a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordQuoteViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/RecordQuoteViewModel.swift
@@ -14,10 +14,9 @@ final class RecordQuoteViewModel: ObservableObject {
@Published var displayQuoteGroups: [QuoteGroup] = []
@Published var searchText: String = ""
@Published var selectedSort: SortOption = .pageAscending
- @Published var highlightKeyword: String? = nil
// MARK: - Internal Variable
- private(set) var quoteGroups: [QuoteGroup] = []
+ private var quoteGroups: [QuoteGroup] = []
var selectedQuote: QuoteVO? = nil
// MARK: - Dependency
@@ -41,9 +40,10 @@ final class RecordQuoteViewModel: ObservableObject {
sortDisplayQuoteGroups()
case .onSubmit:
- searchQuotes()
+ print("검색어: \(searchText)")
case .deleteQuote(let id):
+ print("문장 삭제")
deleteQuote(id: id)
}
@@ -71,6 +71,7 @@ private extension RecordQuoteViewModel {
[weak self] group in
guard let self = self else { return [] }
+ // 3. 구분된 책의 문장을 QuoteGroup으로 생성
for (isbn, quotes) in quoteDict {
group.addTask {
do {
@@ -94,11 +95,12 @@ private extension RecordQuoteViewModel {
return results
} // : withTaskGroup
+
await MainActor.run {
- // 5. 만들어진 QuoteGroup을 반영
+ // 4. 만들어진 QuoteGroup을 반영
self.quoteGroups = quoteGroups
- // 6. 검색어 필터를 진행
- searchQuotes()
+ // 5. QuoteGroup 정렬을 진행
+ sortDisplayQuoteGroups()
}
} catch {
print("문장 로드 중 문제 발생")
@@ -108,7 +110,7 @@ private extension RecordQuoteViewModel {
/// 보여주고자 하는 Quote의 순서를 정렬합니다.
func sortDisplayQuoteGroups() {
- let sortedGroup = self.displayQuoteGroups
+ let sortedGroup = quoteGroups
// 1. 그룹 내부의 메모를 정렬
.map { group in
var sortedGroup = group
@@ -131,51 +133,4 @@ private extension RecordQuoteViewModel {
loadQuoteGroups()
}
}
-
- /// 검색어롤 문장을 필터링 합니다.
- func searchQuotes() {
- // 1. 검색어가 없으면 전체 문장을 보여줌(화이트스페이스, 줄바꿈 제거)
- let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmed.isEmpty else {
- self.searchText = ""
- self.highlightKeyword = nil
- self.displayQuoteGroups = quoteGroups
- self.sortDisplayQuoteGroups()
- return
- }
-
- // 대소문자 구별하지 않음
- let keyword = searchText.lowercased()
-
- // 2. 문장에서 검색어가 포함된 것을 필터링
- let filteredGroups = quoteGroups.compactMap { group -> QuoteGroup? in
- // 2-1. 책 제목을 필터링
- let bookTitleMatched = group.bookTitle.lowercased().contains(keyword)
-
- // 2-2. 메모 내용을 필터링
- let matchedQuotes = group.quotes.filter {
- $0.content.lowercased().contains(keyword)
- }
-
- // 2-3. 책 제목에 포함되면 전부, 내용만 포함되면 필터된 내용만
- if bookTitleMatched || !matchedQuotes.isEmpty {
- return QuoteGroup(
- isbn: group.isbn,
- bookTitle: group.bookTitle,
- quotes: bookTitleMatched ? group.quotes : matchedQuotes
- )
- } else {
- return nil
- }
- }
-
- // 3. 필터한 내용을 저장
- self.displayQuoteGroups = filteredGroups
-
- // 4. 필터한 내용을 정렬
- self.sortDisplayQuoteGroups()
-
- // 5. 하이라이트 키워드 반영
- self.highlightKeyword = trimmed
- }
}
diff --git a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/SummaryViewModel.swift b/B.READ/B.READ/Sources/Presentation/Record/ViewModel/SummaryViewModel.swift
deleted file mode 100644
index 496c891b..00000000
--- a/B.READ/B.READ/Sources/Presentation/Record/ViewModel/SummaryViewModel.swift
+++ /dev/null
@@ -1,132 +0,0 @@
-//
-// SummaryViewModel.swift
-// B.READ
-//
-// Created by 김도연 on 6/10/25.
-//
-
-import Foundation
-import SwiftUI
-
-enum SummaryState {
- case loading, loaded, failed
-}
-
-final class SummaryViewModel: ObservableObject {
- // MARK: - Internal Variable
- let record: RecordDetailVO
- private let memos: [MemoVO]
- private let quotes: [QuoteVO]
- private var currentTask: Task? = nil
-
- // MARK: - State
- @Published var dataState: SummaryState = .loading
- @Published var summary: SummaryVO?
- @Published var memoData: [String] = []
- @Published var quoteData: [String] = []
-
- // MARK: - Dependency
- @Dependency
- private var summaryUseCase: SummaryUseCase
-
- // MARK: - Init
- // 버튼을 통해 생성 페이지로 넘어왔을 때,
- init(record: RecordDetailVO, memos: [MemoVO], quotes: [QuoteVO]) {
- self.record = record
- self.memos = memos
- self.quotes = quotes
-
- memoData = memos.map { $0.content }
- quoteData = quotes.map { $0.content }
-
- generateSummary()
- }
-
- // 이미 만들어진 요약노트를 조회할 때,
- init(id: String, record: RecordDetailVO, memos: [MemoVO], quotes: [QuoteVO]) {
- self.record = record
- self.memos = memos
- self.quotes = quotes
-
- memoData = memos.map { $0.content }
- quoteData = quotes.map { $0.content }
-
- fetchSummary(id)
- }
-
- deinit {
- currentTask?.cancel()
- }
-
- enum Action {
- case cancelTask
- }
-
- func send(_ action: Action) {
- switch action {
- case .cancelTask:
- currentTask?.cancel()
- }
- }
-
-}
-
-private extension SummaryViewModel {
- func generateSummary() {
- currentTask?.cancel()
-
- currentTask = Task{
- do {
- let recordData = record.toEntity(memos: memos, quotes: quotes)
- try Task.checkCancellation()
- let summaryData = try await summaryUseCase.generateSummary(in: recordData)
- try Task.checkCancellation()
-
- await MainActor.run {
- summary = SummaryVO(summaryData)
- dataState = .loaded
- }
-
- try await summaryUseCase.saveSummary(summaryData, in: recordData)
- try Task.checkCancellation()
- } catch {
- if Task.isCancelled {
- print("\(#function) is cancelled")
- return
- }
-
- print(error)
- await MainActor.run {
- dataState = .failed
- }
- }
- }
- }
-
- func fetchSummary(_ id: String) {
- currentTask?.cancel()
-
- currentTask = Task {
- do {
- try Task.checkCancellation()
- let summaryData = try await summaryUseCase.fetchSummary(id: id)
- try Task.checkCancellation()
-
- await MainActor.run {
- summary = SummaryVO(summaryData)
- dataState = .loaded
- }
- } catch {
- if Task.isCancelled {
- print("\(#function) is cancelled")
- return
- }
-
- print(error)
- await MainActor.run {
- dataState = .failed
- }
- }
- }
- }
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Search/VM/BestSellerViewModel.swift b/B.READ/B.READ/Sources/Presentation/Search/VM/BestSellerViewModel.swift
index 8d604853..1e049bfa 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/VM/BestSellerViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/VM/BestSellerViewModel.swift
@@ -18,11 +18,6 @@ final class BestSellerViewModel: ObservableObject {
// MARK: - Dependency
@Dependency private var recommandUseCase: RecommandUseCase
- init(
- ) {
-// print("BestSellerViewModel이 생성되었습니다. ")
- }
-
enum Action {
case onAppear
case cancelTask
diff --git a/B.READ/B.READ/Sources/Presentation/Search/VM/BookViewModel.swift b/B.READ/B.READ/Sources/Presentation/Search/VM/BookViewModel.swift
index 95aeabc4..07601a3e 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/VM/BookViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/VM/BookViewModel.swift
@@ -19,7 +19,6 @@ final class BookViewModel: ObservableObject {
// MARK: - State
@Published var bookState: BookState = .loading
@Published var selectedState: ReadingState = .notStart
- @Published var isSuccess: Bool = false
var currentBook: Book?
var isbn: String
@@ -29,12 +28,10 @@ final class BookViewModel: ObservableObject {
init(isbn: String) {
self.isbn = isbn
-// print("BookViewModel이 생성되었습니다. ")
}
deinit {
currentTask?.cancel()
-// print("BookViewModel이 소멸되었습니다. ")
}
// MARK: - Dependency
diff --git a/B.READ/B.READ/Sources/Presentation/Search/VM/NewRecordViewModel.swift b/B.READ/B.READ/Sources/Presentation/Search/VM/NewRecordViewModel.swift
index 0a74ceb4..469cce12 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/VM/NewRecordViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/VM/NewRecordViewModel.swift
@@ -29,7 +29,6 @@ final class NewRecordViewModel: ObservableObject {
var totalPage: Int
private var pageNum: Int = 0
- private var currentTask: Task? = nil
// MARK: - Dependency
@Dependency private var libraryUseCase: LibraryUseCase
@@ -47,7 +46,6 @@ final class NewRecordViewModel: ObservableObject {
self.reviewText = ""
self.book = book
self.totalPage = book.totalPages
-// print("NewRecordViewModel이 생성되었습니다. ")
}
/// Library에서 Record 수정하는 경우
@@ -63,22 +61,16 @@ final class NewRecordViewModel: ObservableObject {
self.reviewText = recordVO.review
self.totalPage = recordVO.totalPage
self.book = nil
-// print("NewRecordViewModel이 생성되었습니다. ")
- }
-
- deinit {
- currentTask?.cancel()
}
// MARK: - Action
enum Action {
case updateRecord(ReadingState)
case createRecord(ReadingState)
- case pageSubmit(ReadingState)
+ case pageSubmit
case releaseEditorFocus
case focusOnTextField
case releaseAllFocus
- case cancelTask
}
func send(_ action: Action) {
@@ -93,21 +85,17 @@ final class NewRecordViewModel: ObservableObject {
updateRecord(entity)
}
- case let .pageSubmit(state):
+ case .pageSubmit:
isFocused = false
- if state == .reading {
- if let value = Int(page), value >= 0, value <= totalPage {
- pageNum = value
- inValidPageNumber = false
- } else {
- pageNum = 0
- page = ""
- inValidPageNumber = true
- }
- } else {
+ if let value = Int(page), value >= 0, value <= totalPage {
+ pageNum = value
inValidPageNumber = false
+ } else {
+ pageNum = 0
+ page = "0"
+ inValidPageNumber = true
}
-
+
case .releaseEditorFocus:
DispatchQueue.main.async { [weak self] in
self?.isTextEditorFocused = false
@@ -123,9 +111,6 @@ final class NewRecordViewModel: ObservableObject {
self?.isFocused = false
self?.isTextEditorFocused = false
}
-
- case .cancelTask:
- currentTask?.cancel()
}
}
@@ -245,41 +230,23 @@ private extension NewRecordViewModel {
func saveNewRecord(_ record: Record) {
guard let book = self.book else { return }
-
- currentTask?.cancel()
-
- currentTask = Task {
+
+ Task {
do {
- try Task.checkCancellation()
try await libraryUseCase.saveRecord(record: record, book: book)
- try Task.checkCancellation()
await MainActor.run { isSuccess = true }
} catch {
- if Task.isCancelled {
- print("\(#function) is cancelled")
- return
- }
-
print(error)
}
}
}
func updateRecord(_ record: Record) {
- currentTask?.cancel()
-
- currentTask = Task {
+ Task {
do {
- try Task.checkCancellation()
try await libraryUseCase.editRecord(record)
- try Task.checkCancellation()
await MainActor.run { isSuccess = true }
} catch {
- if Task.isCancelled {
- print("\(#function) is cancelled")
- return
- }
-
print(error)
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Search/VM/RecentSearchViewModel.swift b/B.READ/B.READ/Sources/Presentation/Search/VM/RecentSearchViewModel.swift
index a6edf7f1..a2599bb4 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/VM/RecentSearchViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/VM/RecentSearchViewModel.swift
@@ -17,13 +17,8 @@ final class RecentSearchViewModel: ObservableObject {
// MARK: - Dependency
@Dependency private var profileUseCase: ProfileUseCase
- init() {
-// print("RecentSearchViewModel이 생성되었습니다. ")
- }
-
deinit {
currentTask?.cancel()
-// print("RecentSearchViewModel이 소멸되었습니다. ")
}
// MARK: - Action
@@ -73,7 +68,6 @@ private extension RecentSearchViewModel {
try Task.checkCancellation()
let result = try await profileUseCase.fetchRecentKeywords()
try Task.checkCancellation()
-
await MainActor.run {
keywords = result
}
diff --git a/B.READ/B.READ/Sources/Presentation/Search/VM/ScanViewModel.swift b/B.READ/B.READ/Sources/Presentation/Search/VM/ScanViewModel.swift
index 064860b3..3a94ae2d 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/VM/ScanViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/VM/ScanViewModel.swift
@@ -11,14 +11,4 @@ import SwiftUI
final class ScanViewModel: ObservableObject {
@Published var noCamera: Bool = false
@Published var isbnNumber: String = ""
-
- init() {
- self.noCamera = false
- self.isbnNumber = ""
-// print("ScanViewModel이 생성되었습니다. ")
- }
-
- deinit {
-// print("ScanViewModel이 소멸되었습니다. ")
- }
}
diff --git a/B.READ/B.READ/Sources/Presentation/Search/VM/SearchInputViewModel.swift b/B.READ/B.READ/Sources/Presentation/Search/VM/SearchInputViewModel.swift
index 87a7ca23..5f30a410 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/VM/SearchInputViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/VM/SearchInputViewModel.swift
@@ -14,14 +14,6 @@ final class SearchInputViewModel: ObservableObject {
@Published var isSubmitted: Bool = false // 검색어가 제출되었는지 여부
@Published var isFocused: Bool = false // 검색창이 활성화되어있는지 여부
- init() {
-// print("SearchInputViewModel이 생성되었습니다. ")
- }
-
- deinit {
-// print("SearchInputViewModel이 소멸되었습니다. ")
- }
-
enum Action {
case onTapClear
case onSubmitSearch
diff --git a/B.READ/B.READ/Sources/Presentation/Search/VM/SearchResultViewModel.swift b/B.READ/B.READ/Sources/Presentation/Search/VM/SearchResultViewModel.swift
index 477d48c9..ac430869 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/VM/SearchResultViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/VM/SearchResultViewModel.swift
@@ -24,10 +24,10 @@ final class SearchResultViewModel: ObservableObject {
@Published var bookLoadState: DataState = .loading
@Published var recordLoadState: DataState = .loading
@Published var searchKeyword: String = ""
- @Published var totalBookCount: Int = .max
// MARK: - Internal Property
private var curIndex: Int = 1
+ private var totalBookCount: Int = .max
// MARK: - Task Controller
private var bookTask: Task?
@@ -36,14 +36,9 @@ final class SearchResultViewModel: ObservableObject {
// MARK: - Dependency
@Dependency private var searchUseCase: SearchUseCase
- init() {
-// print("SearchResultViewModel이 생성되었습니다. ")
- }
-
deinit {
bookTask?.cancel()
recordTask?.cancel()
-// print("SearchResultViewModel이 소멸되었습니다. ")
}
enum Action {
diff --git a/B.READ/B.READ/Sources/Presentation/Search/View/Components/CustomCameraRepresentable.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Components/CustomCameraRepresentable.swift
index ba6b776d..7fbbad02 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/View/Components/CustomCameraRepresentable.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Components/CustomCameraRepresentable.swift
@@ -176,8 +176,8 @@ class CustomCameraController: UIViewController, AVCaptureMetadataOutputObjectsDe
cameraPreviewLayer?.frame = CGRect(
x: 0,
y: 0,
- width: view.frame.width+1,
- height: view.frame.width+1
+ width: view.frame.width,
+ height: view.frame.width
)
if let previewLayer = cameraPreviewLayer {
@@ -192,10 +192,7 @@ class CustomCameraController: UIViewController, AVCaptureMetadataOutputObjectsDe
) {
guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
let value = object.stringValue,
- currentDetected != value else {
- currentDetected = ""
- return
- }
+ currentDetected != value else { return }
// 중복 방지용 내부 상태 갱신
currentDetected = value
diff --git a/B.READ/B.READ/Sources/Presentation/Search/View/Components/CustomTextEditor.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Components/CustomTextEditor.swift
index 71d97f4b..626dae17 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/View/Components/CustomTextEditor.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Components/CustomTextEditor.swift
@@ -41,6 +41,14 @@ struct CustomTextEditor: UIViewRepresentable {
}
func updateUIView(_ uiView: UITextView, context: Context) {
+ if isFocused != uiView.isFirstResponder {
+ if isFocused {
+ uiView.becomeFirstResponder()
+ } else {
+ uiView.resignFirstResponder()
+ }
+ }
+
if uiView.isFirstResponder {
if uiView.textColor == placeholderColor {
uiView.text = ""
@@ -55,6 +63,7 @@ struct CustomTextEditor: UIViewRepresentable {
uiView.textColor = textColor
}
}
+
uiView.typingAttributes = makeTypingAttributes()
}
diff --git a/B.READ/B.READ/Sources/Presentation/Common/LoadingView.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Components/LoadingView.swift
similarity index 67%
rename from B.READ/B.READ/Sources/Presentation/Common/LoadingView.swift
rename to B.READ/B.READ/Sources/Presentation/Search/View/Components/LoadingView.swift
index c90764c1..3a0de1a4 100644
--- a/B.READ/B.READ/Sources/Presentation/Common/LoadingView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Components/LoadingView.swift
@@ -7,13 +7,8 @@
import SwiftUI
-struct LoadingView: View {
+struct BouncingImageLoadingView: View {
@State private var scale: CGFloat = 1.0
- let text: String?
-
- init(text: String? = nil) {
- self.text = text
- }
var body: some View {
VStack {
@@ -27,30 +22,29 @@ struct LoadingView: View {
scale = 1.2
}
- Text(text ?? "데이터 불러오는 중...")
- .brStyleFont(.pretendard(.regular, size: text == nil ? 16 : 12), lineHeight: 1.2)
+ Text("데이터 불러오는 중...")
+ .brStyleFont(.pretendard(.regular, size: 16), lineHeight: 1.2)
.foregroundColor(.brown7)
.padding(.top, 16)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
- //.background(.backgroundDefault)
+ .background(.backgroundDefault)
.ignoresSafeArea()
}
}
#Preview {
- LoadingView()
+ BouncingImageLoadingView()
}
struct FailedView: View {
- var title: String = "😢 정보를 불러오는 데 실패했어요."
var error: Error? = nil
var desp: String? = nil
var body: some View {
VStack(spacing: 16) {
- Text(title)
- .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1, letterSpacing: -0.02)
+ Text("😢 정보를 불러오는 데 실패했어요.")
+ .font(.headline)
.foregroundStyle(.brown9)
Group {
@@ -61,7 +55,7 @@ struct FailedView: View {
Text(desp)
}
}
- .brStyleFont(.pretendard(.light, size: 14), lineHeight: 1, letterSpacing: -0.02)
+ .font(.caption)
.foregroundColor(.gray7)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
diff --git a/B.READ/B.READ/Sources/Presentation/Search/View/Components/ReadStateButton.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Components/ReadStateButton.swift
index 96d1353e..d9dca535 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/View/Components/ReadStateButton.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Components/ReadStateButton.swift
@@ -185,7 +185,6 @@ struct ReviewInputView: View {
)
.background(Color.clear.onTapGesture(perform: tapGesture))
.frame(height: 100, alignment: .center)
- .tint(.gray9)
}
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Search/View/Main/BookDetailView.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Main/BookDetailView.swift
index c742cd25..21edf9ac 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/View/Main/BookDetailView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Main/BookDetailView.swift
@@ -12,15 +12,15 @@ struct BookDetailView: View {
@StateObject var viewModel: BookViewModel
@EnvironmentObject var coordinator: Coordinator
- init(viewModel: @autoclosure @escaping () -> BookViewModel) {
- self._viewModel = StateObject(wrappedValue: viewModel())
+ init(viewModel: BookViewModel) {
+ self._viewModel = .init(wrappedValue: viewModel)
}
var body: some View {
Group {
switch viewModel.bookState {
case .loading:
- LoadingView()
+ BouncingImageLoadingView()
case .loaded(let bookDetailVO):
loadedView(bookDetailVO)
case .failed(let error):
@@ -34,19 +34,10 @@ struct BookDetailView: View {
.presentationDragIndicator(.hidden)
})
.background(.backgroundDefault, ignoresSafeAreaEdges: .all)
- .alert("저장 성공", isPresented: $viewModel.isSuccess) {
- Button("확인", role: .cancel) {
- DispatchQueue.main.async {
- viewModel.isSuccess = false
- }
- }
- } message: {
- Text("내 책빵에 저장되었습니다.")
- } // : alert
- .onAppear { // onAppear
+ .onAppear {
viewModel.send(.onAppear)
}
- .onDisappear { // onDisappear
+ .onDisappear {
viewModel.send(.cancelTask)
}
}
@@ -96,14 +87,7 @@ struct BookDetailView: View {
coordinator.presentSheet(
.createRecord(
state: $viewModel.selectedState,
- book: book,
- onComplete: { isSuccess in
- if isSuccess {
- DispatchQueue.main.async {
- viewModel.isSuccess = true
- }
- }
- }
+ book: book
)
)
} else {
diff --git a/B.READ/B.READ/Sources/Presentation/Search/View/Main/CreateRecordView.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Main/CreateRecordView.swift
index 7dea3280..31055ef3 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/View/Main/CreateRecordView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Main/CreateRecordView.swift
@@ -9,31 +9,27 @@ import SwiftUI
// MARK: - (S)CreateRecordView
struct CreateRecordView: View {
+ private let layoutPadding: CGFloat = 24
+ let onComplete: (_ isEdit: Bool) -> Void
+
@Binding var selectedState: ReadingState
@StateObject var viewModel: NewRecordViewModel
@EnvironmentObject var coordinator: Coordinator
- private let layoutPadding: CGFloat = 24
- let onComplete: (_ isEdit: Bool) -> Void
-
- // MARK: - Inits
- init(state: Binding, viewModel: @autoclosure @escaping () -> NewRecordViewModel) {
- self._selectedState = state
- self._viewModel = StateObject(wrappedValue: viewModel())
- self.onComplete = { _ in }
+ init(state: Binding, viewModel: NewRecordViewModel) {
+ self.init(state: state, viewModel: viewModel, onComplete: { _ in })
}
-
+
init(
state: Binding,
- viewModel: @autoclosure @escaping () -> NewRecordViewModel,
+ viewModel: NewRecordViewModel,
onComplete: @escaping (_ isEdit: Bool) -> Void
) {
self._selectedState = state
- self._viewModel = StateObject(wrappedValue: viewModel())
+ self._viewModel = .init(wrappedValue: viewModel)
self.onComplete = onComplete
}
-
- // MARK: - body
+
var body: some View {
Group {
ZStack(alignment: .topTrailing) {
@@ -51,13 +47,12 @@ struct CreateRecordView: View {
.onChange(of: selectedState) { _, _ in
viewModel.send(.releaseAllFocus)
}
-
+
stateContentView()
.padding(.top, layoutPadding)
BottomButton(buttonTitle: "저장하기") {
- viewModel.send(.pageSubmit(selectedState))
-
+ viewModel.send(.pageSubmit)
if !viewModel.inValidPageNumber {
if viewModel.recordVO != nil {
viewModel.send(.updateRecord(selectedState))
@@ -77,13 +72,16 @@ struct CreateRecordView: View {
.clipShape(
RoundedCorner(radius: 16, corners: [.topLeft, .topRight])
)
+
}
.ignoresSafeArea()
.onChange(of: viewModel.isSuccess) { _, newValue in
- DispatchQueue.main.async {
- let isEdit = newValue
- onComplete(isEdit)
- coordinator.dismissSheet()
+ if newValue {
+ DispatchQueue.main.async {
+ let isEdit = viewModel.recordVO != nil
+ onComplete(isEdit)
+ coordinator.dismissSheet()
+ }
}
}
.alert("저장 실패", isPresented: $viewModel.inValidPageNumber) {
@@ -92,11 +90,8 @@ struct CreateRecordView: View {
}
} message: {
Text("올바른 페이지 번호가 아닙니다.\n1 ~ \(viewModel.totalPage) 사이의 숫자를 입력해주세요")
- } //: alert
- .onDisappear {
- viewModel.send(.cancelTask)
}
-
+
}
// MARK: - (F)stateContentView
@@ -120,7 +115,7 @@ struct CreateRecordView: View {
page: $viewModel.page,
isFocused: $viewModel.isFocused,
maxPage: viewModel.totalPage) {
- viewModel.send(.pageSubmit(selectedState))
+ viewModel.send(.pageSubmit)
}
.topLeadingPadding()
}
diff --git a/B.READ/B.READ/Sources/Presentation/Search/View/Main/ScanView.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Main/ScanView.swift
index 5a38ab7e..e5298b83 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/View/Main/ScanView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Main/ScanView.swift
@@ -12,13 +12,14 @@ struct ScanView: View {
@StateObject var viewModel: ScanViewModel
@EnvironmentObject var coordinator: Coordinator
- init(viewModel: @autoclosure @escaping () -> ScanViewModel) {
- self._viewModel = StateObject(wrappedValue: viewModel())
+ init(viewModel: ScanViewModel) {
+ self._viewModel = .init(wrappedValue: viewModel)
}
var body: some View {
VStack {
CustomCameraRepresentable(isbnNumber: $viewModel.isbnNumber, noCamera: $viewModel.noCamera)
+
.frame(height: 400, alignment: .top)
.frame(maxWidth: .infinity)
.padding(.top, 16)
@@ -63,9 +64,6 @@ struct ScanView: View {
Text("카메라를 사용할 수 없습니다.")
}
.background(.backgroundDefault, ignoresSafeAreaEdges: .all)
- .onAppear {
- viewModel.isbnNumber = ""
- }
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Search/View/Main/SearchResultView.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Main/SearchResultView.swift
index 302cb9be..f96bf60c 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/View/Main/SearchResultView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Main/SearchResultView.swift
@@ -49,47 +49,38 @@ struct SearchTabContentView: View {
var body: some View {
Group {
- let text = "\"\(viewModel.searchKeyword)\"에 일치하는 검색 결과가 없습니다."
-
if viewModel.selectedTabIndex == 0 {
Group {
switch viewModel.bookLoadState {
case .loading:
if viewModel.bookResults.isEmpty {
- LoadingView()
+ BouncingImageLoadingView()
} else {
- VStack(spacing: 4) {
- SearchResultCountView(totalBookCount: viewModel.totalBookCount)
-
- SearchListView(
- items: viewModel.bookResults,
- layoutPadding: 24,
- listPadding: 16,
- onTap: { coordinator.push(.searchBook(isbn: $0.isbn)) },
- onAppearNearBottom: { viewModel.send(.fetchMoreBooks($0)) },
- content: { book in
- BookSearchCell(data: book)
- }
- )
- }
+ SearchListView(
+ items: viewModel.bookResults,
+ layoutPadding: 24,
+ listPadding: 16,
+ onTap: { coordinator.push(.searchBook(isbn: $0.isbn)) },
+ onAppearNearBottom: { viewModel.send(.fetchMoreBooks($0)) },
+ content: { book in
+ BookSearchCell(data: book)
+ }
+ )
}
case .loaded:
if viewModel.bookResults.isEmpty {
- FailedView(desp: text)
+ FailedView(desp: "일치하는 검색 결과가 없습니다.")
} else {
- VStack(spacing: 4) {
- SearchResultCountView(totalBookCount: viewModel.totalBookCount)
- SearchListView(
- items: viewModel.bookResults,
- layoutPadding: 24,
- listPadding: 16,
- onTap: { coordinator.push(.searchBook(isbn: $0.isbn)) },
- onAppearNearBottom: { viewModel.send(.fetchMoreBooks($0)) },
- content: { book in
- BookSearchCell(data: book)
- }
- )
- }
+ SearchListView(
+ items: viewModel.bookResults,
+ layoutPadding: 24,
+ listPadding: 16,
+ onTap: { coordinator.push(.searchBook(isbn: $0.isbn)) },
+ onAppearNearBottom: { viewModel.send(.fetchMoreBooks($0)) },
+ content: { book in
+ BookSearchCell(data: book)
+ }
+ )
}
case .failed(let error):
FailedView(error: error)
@@ -100,24 +91,21 @@ struct SearchTabContentView: View {
Group {
switch viewModel.recordLoadState {
case .loading:
- LoadingView()
+ BouncingImageLoadingView()
case .loaded:
if viewModel.recordResults.isEmpty {
- FailedView(desp: text)
+ FailedView(desp: "일치하는 검색 결과가 없습니다.")
} else {
- VStack(spacing: 4) {
- SearchResultCountView(totalBookCount: viewModel.recordResults.count)
- SearchListView(
- items: viewModel.recordResults,
- layoutPadding: 24,
- listPadding: 16,
- onTap: { coordinator.push(.libraryDetail(id: $0.id)) },
- onAppearNearBottom: nil,
- content: { record in
- RecordSearchCell(data: record)
- }
- )
- }
+ SearchListView(
+ items: viewModel.recordResults,
+ layoutPadding: 24,
+ listPadding: 16,
+ onTap: { coordinator.push(.libraryDetail(id: $0.id)) },
+ onAppearNearBottom: nil,
+ content: { record in
+ RecordSearchCell(data: record)
+ }
+ )
}
case .failed(let error):
FailedView(error: error)
@@ -164,29 +152,3 @@ struct SearchListView: View {
}
}
}
-
-struct SearchResultCountView: View {
- let totalBookCount: Int
-
- var body: some View {
- HStack(spacing: 4) {
- Text("총")
- .brStyleFont(.pretendard(.light, size: 14), lineHeight: 1.2)
- .foregroundStyle(.gray5)
-
- Text("\(totalBookCount)")
- .brStyleFont(.pretendard(.regular, size: 14), lineHeight: 1.2)
- .foregroundStyle(.brown9)
-
- Text("개의 검색결과")
- .brStyleFont(.pretendard(.light, size: 14), lineHeight: 1.2)
- .foregroundStyle(.gray5)
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.horizontal, 24)
- }
-}
-
-#Preview {
- SearchResultCountView(totalBookCount: 123)
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Search/View/Main/SearchView.swift b/B.READ/B.READ/Sources/Presentation/Search/View/Main/SearchView.swift
index 4cb285b5..20b874e2 100644
--- a/B.READ/B.READ/Sources/Presentation/Search/View/Main/SearchView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Search/View/Main/SearchView.swift
@@ -10,28 +10,38 @@ import Foundation
// MARK: - (S)SearchView
struct SearchView: View {
- @StateObject private var inputViewModel = SearchInputViewModel()
- @StateObject private var resultViewModel = SearchResultViewModel()
- @StateObject private var recentSearchViewModel = RecentSearchViewModel()
- @StateObject private var bestSellerViewModel = BestSellerViewModel()
+ @StateObject private var inputViewModel: SearchInputViewModel
+ @StateObject private var resultViewModel: SearchResultViewModel
+ @StateObject private var recentSearchViewModel: RecentSearchViewModel
+ @StateObject private var bestSellerViewModel: BestSellerViewModel
@EnvironmentObject var coordinator: Coordinator
private let layoutSize: CGFloat = 16
private let horizontalPadding: CGFloat = 24
+ init(
+ inputViewModel: SearchInputViewModel,
+ resultViewModel: SearchResultViewModel,
+ recentSearchViewModel: RecentSearchViewModel,
+ bestSellerViewModel: BestSellerViewModel
+ ) {
+ self._inputViewModel = .init(wrappedValue: inputViewModel)
+ self._resultViewModel = .init(wrappedValue: resultViewModel)
+ self._recentSearchViewModel = .init(wrappedValue: recentSearchViewModel)
+ self._bestSellerViewModel = .init(wrappedValue: bestSellerViewModel)
+ }
+
var body: some View {
VStack(alignment: .center, spacing: layoutSize) {
-
if !inputViewModel.isFocused && !inputViewModel.isSubmitted {
- LogoView()
+ logoView
.transition(.opacity)
}
// 검색창은 한 개만 존재해야함
searchBarSection
.padding(.top, inputViewModel.isFocused || inputViewModel.isSubmitted ? layoutSize : 0)
- .padding(.horizontal, horizontalPadding)
SearchContentView(
inputViewModel: inputViewModel,
@@ -141,9 +151,6 @@ struct SearchContentView: View {
SearchResultView(
viewModel: resultViewModel
)
- .onDisappear {
- resultViewModel.send(.clearSelect)
- }
} else {
VStack(alignment: .leading, spacing: layoutSize) {
@@ -152,7 +159,7 @@ struct SearchContentView: View {
.foregroundStyle(.black)
if bestSellerViewModel.bestBookList.isEmpty {
- LoadingView()
+ BouncingImageLoadingView()
} else {
BestSellerView(bookList: bestSellerViewModel.bestBookList) { book in
coordinator.push(.searchBook(isbn: book.isbn))
diff --git a/B.READ/B.READ/Sources/Presentation/Sentence/PageInputView.swift b/B.READ/B.READ/Sources/Presentation/Sentence/PageInputView.swift
index b9bd22c3..4922df15 100644
--- a/B.READ/B.READ/Sources/Presentation/Sentence/PageInputView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Sentence/PageInputView.swift
@@ -8,115 +8,130 @@
import SwiftUI
struct PageInputView: View {
+ let mode: SentenceInputMode
+ let sentence: String
+
+ @State private var pageText: String = "0"
+ @StateObject private var viewModel: SentenceViewModel
@EnvironmentObject var coordinator: Coordinator
- @StateObject var viewModel: PageInputViewModel
+ @State private var showInvalidAlert = false
@FocusState private var isFocused: Bool
- // MARK: - Init
- init(viewModel: @autoclosure @escaping () -> PageInputViewModel) {
- _viewModel = StateObject(wrappedValue: viewModel())
+ init(mode: SentenceInputMode, sentence: String) {
+ self.mode = mode
+ self.sentence = sentence
+ _viewModel = StateObject(wrappedValue: SentenceViewModel(mode: mode))
}
+
var body: some View {
- VStack(spacing: 24) {
- VStack(alignment: .leading, spacing: 8) {
- Text("페이지를 입력해 주세요")
- .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1.2)
+ let isValidPage: Bool = {
+ guard let limit = viewModel.maxPage else {
+ return false
+ }
+
+ guard let number = Int(pageText) else {
+ return false
+ }
+
+ return (1...limit).contains(number)
+ }()
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text("페이지를 입력해 주세요")
+ .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1.2)
+ HStack(spacing: 0) {
+ RoundedTextField(
+ type: .pages,
+ placeholder: "0",
+ text: $pageText,
+ isValid: isValidPage
+ )
+ .focused($isFocused)
- HStack(spacing: 16) {
- RoundedTextField(
- type: .pages,
- placeholder: "0",
- text: $viewModel.page,
- isValid: viewModel.isValid
- )
- .focused($isFocused)
- .onChange(of: viewModel.page) {
- viewModel.send(.updatePage)
- }
-
- Text("쪽")
- .brStyleFont(.pretendard(.medium, size: 16), lineHeight: 1.2)
- } // : HStack
+ Text("쪽")
+ .brStyleFont(.pretendard(.medium, size: 16), lineHeight: 1.2)
+ .padding(.leading, 16)
}
- .padding(.horizontal, 24)
- .padding(.top, 16)
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(.green1)
- ScrollView {
- Text(viewModel.sentence)
- .brStyleFont(
- .pretendard(.semiBold, size: 14),
- lineHeight: 1,
- letterSpacing: -0.025
- )
+ ScrollView(.vertical, showsIndicators: true) {
+ Text(sentence)
+ .brStyleFont(.pretendard(.semiBold, size: 14), lineHeight: 1, letterSpacing: -0.025)
.frame(maxWidth: .infinity, alignment: .leading)
- .padding(16)
- } // : ScrollView
- .scrollIndicators(.hidden)
- } // : ZStack
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+ }
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
.frame(height: 134)
- .padding(.horizontal, 24)
-
- } // : VStack
+ .padding(.top, 24)
+ }
.frame(maxHeight: .infinity, alignment: .top)
- .background(Color.backgroundDefault)
- .onTapGesture {
- self.hideKeyboard()
+ .padding(.top, 16)
+ .padding(.horizontal, 24)
+ .onChange(of: pageText, { oldValue, newValue in
+ viewModel.page = Int(newValue)
+ })
+ .onChange(of: viewModel.didSubmitSuccess) {
+ if viewModel.didSubmitSuccess {
+ coordinator.pop()
+ coordinator.pop()
+ }
}
- .task {
- await Task.yield()
- isFocused = true
- } // : task - 페이지입력 텍스트 필드 focus
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("저장") {
+ viewModel.content = sentence
+ guard viewModel.page != nil else {
+ showInvalidAlert = true
+ return
+ }
+ guard isValidPage else {
+ showInvalidAlert = true
+ return
+ }
viewModel.send(.submit)
}
.brStyleFont(.pretendard(.regular, size: 16), lineHeight: 1.1)
.foregroundStyle(.green6)
- .disabled(!viewModel.isValid)
- .opacity(viewModel.isValid ? 1 : 0)
- .animation(.easeInOut(duration: 0.2), value: viewModel.isValid)
- }
- } // : toolbar
- .onChange(of: viewModel.didSubmitSuccess) {
- if viewModel.didSubmitSuccess {
- coordinator.pop()
- coordinator.pop()
}
}
- .alert("저장 실패", isPresented: $viewModel.showErrorAlert) {
- Button("확인", role: .cancel) { }
+ .alert("저장 실패", isPresented: $showInvalidAlert) {
+ Button("확인", role: .cancel) {
+ viewModel.page = nil
+ pageText = ""
+ isFocused = true
+ }
} message: {
- if let error = viewModel.errorMessage {
- Text(error)
+ Text("올바른 페이지 번호가 아닙니다.")
+ }
+ .animation(.easeInOut(duration: 0.2), value: showInvalidAlert)
+ .background(Color.backgroundDefault)
+ .task {
+ await Task.yield()
+ isFocused = true
+ }
+ .onAppear {
+ if viewModel.content.isEmpty {
+ viewModel.content = sentence
+ }
+ if pageText.isEmpty {
+ pageText = viewModel.page.map(String.init) ?? ""
}
- } // : alert
- .animation(.easeInOut(duration: 0.2), value: viewModel.showErrorAlert)
+ }
}
}
#Preview {
- let record = RecordDetailVO(
- record: DummyData.dummyRecords[1],
- book: DummyData.dummyBooks[1]
- )
- let quote = QuoteVO(
- id: "1",
- isbn: record.isbn,
- content: "테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트",
- page: 45, record: record
- )
-
+ let record = RecordDetailVO(record: DummyData.dummyRecords[1], book: DummyData.dummyBooks[1])
PreviewableContainer {
- CoordinatorContainer {
- NavigationStack {
- PageInputView(viewModel: .init(record: record, quote: quote))
- }
+ NavigationStack {
+ PageInputView(mode: .create(record: record), sentence: "프리뷰 테스트 문장")
}
}
+
}
diff --git a/B.READ/B.READ/Sources/Presentation/Sentence/PageInputViewModel.swift b/B.READ/B.READ/Sources/Presentation/Sentence/PageInputViewModel.swift
deleted file mode 100644
index aa3ccfe4..00000000
--- a/B.READ/B.READ/Sources/Presentation/Sentence/PageInputViewModel.swift
+++ /dev/null
@@ -1,82 +0,0 @@
-//
-// PageInputViewModel.swift
-// B.READ
-//
-// Created by 심근웅 on 6/9/25.
-//
-
-import Foundation
-import SwiftUI
-
-final class PageInputViewModel: ObservableObject {
- // MARK: - State
- @Published var page: String = ""
- @Published private(set) var isValid: Bool
- @Published private(set) var didSubmitSuccess: Bool = false
- @Published private(set) var errorMessage: String?
- @Published var showErrorAlert: Bool = false
-
- // MARK: - Internal Variables
- private let record: RecordDetailVO
- private var quote: QuoteVO
- var sentence: String { quote.content }
-
- // MARK: - Initializer
- init(record: RecordDetailVO, quote: QuoteVO) {
- self.record = record
- self.quote = quote
- self.page = quote.page.description
- self.isValid = quote.page <= record.totalPage && quote.page > 0
- }
-
- // MARK: - Depenency
- @Dependency private var quoteUseCase: QuoteUseCase
-
- // MARK: - Actions
- enum Action {
- case updatePage
- case submit
- }
-
- func send(_ action: Action) {
- switch action {
- case .updatePage:
- self.performUpdatePage()
-
- case .submit:
- self.performSubmit()
- }
- }
-}
-
-private extension PageInputViewModel {
- /// 페이지가 업데이트 됨에 따라, isValid 업데이트
- func performUpdatePage() {
- if let page = self.page.toInt() {
- self.isValid = page > 0 && page <= self.record.totalPage
- self.quote.page = page
- }
- }
-
- /// 작성/수정한 Quote를 저장합니다.
- func performSubmit() {
- Task {
- let quote = quote.toEntity()
- let record = record.toEntity()
-
- do {
- try await quoteUseCase.saveQuote(quote, in: record)
- await MainActor.run {
- errorMessage = nil
- didSubmitSuccess = true
- }
- } catch {
- await MainActor.run {
- errorMessage = error.localizedDescription
- showErrorAlert = true
- }
- return
- }
- }
- }
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Sentence/SentenceInputView.swift b/B.READ/B.READ/Sources/Presentation/Sentence/SentenceInputView.swift
index ae4d7a5e..5d1c7d28 100644
--- a/B.READ/B.READ/Sources/Presentation/Sentence/SentenceInputView.swift
+++ b/B.READ/B.READ/Sources/Presentation/Sentence/SentenceInputView.swift
@@ -8,13 +8,22 @@
import SwiftUI
struct SentenceInputView: View {
+ let mode: SentenceInputMode
+
@EnvironmentObject var coordinator: Coordinator
- @StateObject var viewModel: SentenceInputViewModel
+ @StateObject var viewModel: SentenceViewModel
@FocusState private var isEditorFocused: Bool
+ @State private var showPageAlert = false
+
+ private var trimmedContent: String {
+ viewModel.content.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
- // MARK: - Init
- init(viewModel: @autoclosure @escaping () -> SentenceInputViewModel) {
- _viewModel = StateObject(wrappedValue: viewModel())
+ init(mode: SentenceInputMode) {
+ self.mode = mode
+ _viewModel = StateObject(
+ wrappedValue: SentenceViewModel(mode: mode)
+ )
}
var body: some View {
@@ -42,53 +51,56 @@ struct SentenceInputView: View {
.padding(.vertical, 12)
.allowsHitTesting(false)
}
- } // : ZStack
- } // : VStack
+ }
+ }
.frame(maxHeight: .infinity, alignment: .top)
.padding(.top, 16)
.padding(.horizontal, 24)
- .background(Color.backgroundDefault)
- .onTapGesture {
- self.hideKeyboard()
- }
- .task {
- await Task.yield()
- isEditorFocused = true
- } // : task - 페이지입력 텍스트 필드 focus
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("다음") {
- viewModel.send(.submit)
- coordinator.push(.pageInput(record: viewModel.record, quote: viewModel.quote))
+ guard viewModel.maxPage != nil else {
+ showPageAlert = true
+ return
+ }
+ coordinator.push(
+ .pageInput(mode: mode,
+ sentence: trimmedContent)
+ )
}
.brStyleFont(.pretendard(.regular, size: 16), lineHeight: 1.1)
.foregroundStyle(.green6)
- .disabled(viewModel.trimmedContent.isEmpty)
- .opacity(viewModel.trimmedContent.isEmpty ? 0 : 1)
- .animation(.easeInOut(duration: 0.2), value: viewModel.trimmedContent.isEmpty)
- } // : ToolbarItem
- } // : toolbar
+ .disabled(trimmedContent.isEmpty)
+ .opacity(trimmedContent.isEmpty ? 0 : 1)
+ }
+ }
+ .alert("페이지 정보를 불러오는 중입니다",
+ isPresented: $showPageAlert) {
+ Button("확인", role: .cancel) { }
+ } message: {
+ Text("잠시 후 다시 시도해 주세요.")
+ }
+ .background(Color.backgroundDefault)
+ .onTapGesture {
+ self.hideKeyboard()
+ }
+ }
+}
+
+extension View {
+ func hideKeyboard() {
+ UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
+ to: nil, from: nil, for: nil)
}
}
#Preview {
- let record = RecordDetailVO(
- record: DummyData.dummyRecords[1],
- book: DummyData.dummyBooks[1]
- )
- let quote = QuoteVO(
- id: "1",
- isbn: record.isbn,
- content: "테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트 테스트",
- page: 45, record: record
- )
-
PreviewableContainer {
- CoordinatorContainer {
- NavigationStack {
-// SentenceInputView(viewModel: .init(mode: .create(record: record)))
- SentenceInputView(viewModel: .init(mode: .edit(record: record, quote: quote)))
- }
+ let dummy = Coordinator()
+ let record = RecordDetailVO(record: DummyData.dummyRecords[1], book: DummyData.dummyBooks[1])
+ NavigationStack {
+ SentenceInputView(mode: .create(record: record))
}
+ .environmentObject(dummy)
}
}
diff --git a/B.READ/B.READ/Sources/Presentation/Sentence/SentenceInputViewModel.swift b/B.READ/B.READ/Sources/Presentation/Sentence/SentenceInputViewModel.swift
deleted file mode 100644
index e7ed3751..00000000
--- a/B.READ/B.READ/Sources/Presentation/Sentence/SentenceInputViewModel.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-//
-// SentenceViewModel.swift
-// B.READ
-//
-// Created by 도민준 on 2025/05/25.
-//
-
-import Foundation
-
-/// 새 문장 작성(create) 혹은 기존 문장 수정(edit) 모드 구분
-enum SentenceInputMode: Hashable {
- case create(record: RecordDetailVO)
- case edit(record: RecordDetailVO, quote: QuoteVO)
-}
-
-//@MainActor
-final class SentenceInputViewModel: ObservableObject {
-
- // MARK: - State
- @Published var content: String = "" // 작성한 내용
-
- // MARK: - Internal Variables
- private let mode: SentenceInputMode
- let record: RecordDetailVO
- private(set) var quote: QuoteVO
- var trimmedContent: String {
- content.trimmingCharacters(in: .whitespacesAndNewlines)
- }
-
- // MARK: - Initializer
- init(mode: SentenceInputMode) {
- self.mode = mode
-
- switch mode {
- case .create(let record):
- self.record = record
- self.quote = QuoteVO(
- id: UUID().uuidString,
- isbn: record.isbn,
- content: "",
- page: 0,
- record: record
- )
-
- case .edit(let record, let quote):
- self.record = record
- self.quote = quote
- self.content = quote.content
- }
- }
-
- // MARK: - Actions
- enum Action {
- case submit
- }
-
- func send(_ action: Action) {
- switch action {
- case .submit:
- /// 다음 버튼을 누르면 현재까지 작성한 trim String을 Quote에 저장
- self.quote.content = trimmedContent
- }
- }
-}
diff --git a/B.READ/B.READ/Sources/Presentation/Sentence/SentenceViewModel.swift b/B.READ/B.READ/Sources/Presentation/Sentence/SentenceViewModel.swift
new file mode 100644
index 00000000..d47935f0
--- /dev/null
+++ b/B.READ/B.READ/Sources/Presentation/Sentence/SentenceViewModel.swift
@@ -0,0 +1,161 @@
+//
+// SentenceViewModel.swift
+// B.READ
+//
+// Created by 도민준 on 2025/05/25.
+//
+
+import Foundation
+
+/// 새 문장 작성(create) 혹은 기존 문장 수정(edit) 모드 구분
+enum SentenceInputMode: Hashable {
+ case create(record: RecordDetailVO)
+ case edit(record: RecordDetailVO, quote: QuoteVO)
+}
+
+@MainActor
+final class SentenceViewModel: ObservableObject {
+ // MARK: - State
+ @Published var content: String
+ @Published var page: Int?
+ @Published var maxPage: Int?
+ @Published var isSubmitting: Bool = false
+ @Published var errorMessage: String?
+ @Published var didSubmitSuccess: Bool = false
+
+ // MARK: - Internal Variables
+ private let mode: SentenceInputMode
+ private let record: RecordDetailVO
+
+ // MARK: - Depenency
+ @Dependency private var quoteUseCase: QuoteUseCase
+
+ // MARK: - Actions
+ enum Action {
+ case updateContent(String)
+ case updatePage(Int?)
+ case submit
+ }
+
+ func send(_ action: Action) {
+ switch action {
+ case .updateContent(let newText):
+ content = newText
+ case .updatePage(let newPage):
+ page = newPage
+ case .submit:
+ performSubmit()
+ }
+ }
+
+ // MARK: - Initializer
+ init(mode: SentenceInputMode) {
+ self.mode = mode
+ switch mode {
+ case .create(let record):
+ self.content = ""
+ self.page = nil
+ self.record = record
+ self.maxPage = record.totalPage
+
+ case .edit(let record, let quote):
+ self.content = quote.content
+ self.page = quote.page
+ self.record = record
+ self.maxPage = record.totalPage
+ }
+ }
+}
+
+
+// MARK: - Private Helpers
+private extension SentenceViewModel {
+ func performSubmit() {
+ didSubmitSuccess = false
+
+ Task {
+ // 1) 내용 검증
+ let trimmed = content.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else {
+ print("[⚠️] 내용 검증 실패: 빈 문자열")
+ await MainActor.run {
+ self.errorMessage = QuoteUseCaseError.emptyContent.localizedDescription
+ }
+ return
+ }
+ print("[✅] 내용 검증 통과: '\(trimmed)'")
+
+ // 2) 페이지 숫자 변환 검증
+ guard let page = page else {
+ print("[⚠️] 숫자 변환 실패: page is nil")
+ await MainActor.run {
+ self.errorMessage = "페이지를 숫자로 입력해주세요."
+ }
+ return
+ }
+ print("[✅] 숫자 변환 통과: page = \(page)")
+
+ // 3) 로컬 범위 검증 (1…maxPage)
+ if let limit = maxPage, !(1...limit).contains(page) {
+ print("[⚠️] 로컬 범위 검증 실패: \(page) not in 1...\(limit)")
+ await MainActor.run {
+ self.errorMessage = "페이지는 1-\(limit) 사이여야 합니다."
+ }
+ return
+ }
+ if let limit = maxPage {
+ print("[✅] 로컬 범위 검증 통과: \(page) in 1...\(limit)")
+ } else {
+ print("[ℹ️] maxPage가 아직 로드되지 않음 (검증 건너뛰기)")
+ }
+
+ // 4) ID 결정 및 모델 생성
+ let id: String
+ let isbn = self.record.isbn
+ switch mode {
+ case .create:
+ id = UUID().uuidString
+ print("[ℹ️] 생성 모드: 새로운 id=\(id)")
+ case .edit(_, let quote):
+ id = quote.id
+ print("[ℹ️] 수정 모드: 기존 id=\(id)")
+ }
+ let model = Quote(id: id, isbn: isbn, content: trimmed, page: page)
+ let recordEntity = self.record.toEntity()
+ // 5) 제출 상태 갱신
+ await MainActor.run {
+ self.isSubmitting = true
+ self.errorMessage = nil
+ }
+ defer {
+ Task { await MainActor.run { self.isSubmitting = false } }
+ }
+ print("[ℹ️] isSubmitting = true")
+ // 6) UseCase 호출
+ do {
+ switch mode {
+ case .create:
+ print("[ℹ️] QuoteUseCase.addQuote 호출 시도")
+ try await quoteUseCase.addQuote(model, in: recordEntity)
+ print("[✅] addQuote 성공")
+ await print(try quoteUseCase.fetchAllQuotes())
+ case .edit:
+ print("[ℹ️] QuoteUseCase.updateQuote 호출 시도")
+ try await quoteUseCase.updateQuote(model)
+ print("[✅] updateQuote 성공")
+ }
+ // 성공 시점에 didSubmitSuccess를 true로 변경
+ await MainActor.run {
+ self.didSubmitSuccess = true
+
+ }
+ print("[🎉] didSubmitSuccess = true")
+ } catch {
+ print("[❌] UseCase 호출 중 에러 발생:", error.localizedDescription)
+ await MainActor.run {
+ self.errorMessage = error.localizedDescription
+ }
+ }
+ }
+ }
+}
diff --git a/B.READ/B.READ/Sources/Presentation/Setting/SettingViewModel.swift b/B.READ/B.READ/Sources/Presentation/Setting/SettingViewModel.swift
index 53a767de..369786a1 100644
--- a/B.READ/B.READ/Sources/Presentation/Setting/SettingViewModel.swift
+++ b/B.READ/B.READ/Sources/Presentation/Setting/SettingViewModel.swift
@@ -56,7 +56,6 @@ private extension SettingViewModel {
Task {
do {
let userInfo = try await profileUseCase.fetchUserInfo()
- print(userInfo.lastStreakUpdatedAt)
await MainActor.run {
self.nicknameText = userInfo.nickname
self.selectedCategories = Set(userInfo.categories.compactMap { CategoryType(rawValue: $0.id) })
diff --git a/B.READ/B.READ/Sources/Shared/SharedQuote.swift b/B.READ/B.READ/Sources/Shared/SharedQuote.swift
new file mode 100644
index 00000000..1117ddbf
--- /dev/null
+++ b/B.READ/B.READ/Sources/Shared/SharedQuote.swift
@@ -0,0 +1,14 @@
+//
+// SharedQuote.swift
+// B.READ
+//
+// Created by 도민준 on 6/9/25.
+//
+
+import Foundation
+
+struct SharedQuote: Codable, Identifiable {
+ let id: String
+ let content: String // 문장
+ let bookTitle: String // 제목
+}
diff --git a/B.READ/B.READ/Sources/Shared/SharedQuotesStore.swift b/B.READ/B.READ/Sources/Shared/SharedQuotesStore.swift
new file mode 100644
index 00000000..d2dd05d0
--- /dev/null
+++ b/B.READ/B.READ/Sources/Shared/SharedQuotesStore.swift
@@ -0,0 +1,35 @@
+//
+// SharedQuotesStore.swift
+// B.READ
+//
+// Created by 도민준 on 6/9/25.
+//
+
+import Foundation
+
+/// 위젯과 본앱이 함께 쓰는 “문장 + 책제목” 캐시 저장소
+enum SharedQuotesStore {
+
+ private static let suiteID = "group.BREAD"
+ private static let key = "savedQuotes"
+
+ private static var defaults: UserDefaults? {
+ UserDefaults(suiteName: suiteID)
+ }
+
+ // MARK: - Public API
+ /// 전체 문장 덤프를 저장 (본앱 → 호출)
+ static func save(_ quotes: [SharedQuote]) throws {
+ let data = try JSONEncoder().encode(quotes)
+ defaults?.set(data, forKey: key)
+ }
+
+ /// 저장된 문장 배열을 로드 (위젯 → 호출)
+ static func load() -> [SharedQuote] {
+ guard
+ let data = defaults?.data(forKey: key),
+ let quotes = try? JSONDecoder().decode([SharedQuote].self, from: data)
+ else { return [] }
+ return quotes
+ }
+}
diff --git a/B.READ/B.READ/Sources/Util/Extensions/Date+.swift b/B.READ/B.READ/Sources/Util/Extensions/Date+.swift
index 09603668..33797c78 100644
--- a/B.READ/B.READ/Sources/Util/Extensions/Date+.swift
+++ b/B.READ/B.READ/Sources/Util/Extensions/Date+.swift
@@ -8,25 +8,6 @@
import Foundation
extension Date {
-
- static private let configuredCalendar: Calendar = {
- var calendar = Calendar.current
- calendar.locale = .current
- calendar.timeZone = .current
- return calendar
- }()
-
- /// 요일을 Int로 반환합니다.
- var weekdayInt: Int {
- Self.configuredCalendar.component(.weekday, from: self)
- }
-
- /// Calendar.current의 주(weekOfYear) 기준으로
- /// 이 날짜가 오늘을 포함한 같은 주(일요일~토요일)에 속하는지 여부
- var isInCurrentWeek: Bool {
- Self.configuredCalendar.isDate(self, equalTo: Date(), toGranularity: .weekOfYear)
- }
-
enum DateFormatType: String {
case dotSeparated = "yyyy. MM. dd"
case dotSeparatedFull = "yyyy. MM. dd. (E)"
@@ -45,12 +26,4 @@ extension Date {
Self.dateFormatter.dateFormat = dateFormatType.rawValue
return Self.dateFormatter.string(from: self)
}
-
- /// 두 날짜의 연·월·일(달력 컴포넌트)이 같은지 비교합니다.
- /// - Parameter other: 비교 대상 날짜
- /// - Returns: 같은 날짜(년·월·일)이면 true
- func isSameDay(as other: Date) -> Bool {
- return Self.configuredCalendar.dateComponents([.year, .month, .day], from: self)
- == Self.configuredCalendar.dateComponents([.year, .month, .day], from: other)
- }
}
diff --git a/B.READ/B.READ/Sources/Util/Extensions/String+.swift b/B.READ/B.READ/Sources/Util/Extensions/String+.swift
index 9b3028a1..5b4c69d9 100644
--- a/B.READ/B.READ/Sources/Util/Extensions/String+.swift
+++ b/B.READ/B.READ/Sources/Util/Extensions/String+.swift
@@ -32,10 +32,4 @@ extension String {
formatter.dateFormat = format
return formatter.date(from: self)
}
-
- /// 문자열을 Int로 변환합니다.
- /// - Returns: 변환된 Int 값 (nil 반환 가능)
- func toInt() -> Int? {
- return Int(self)
- }
}
diff --git a/B.READ/B.READ/Sources/Util/Extensions/StringProtocol+.swift b/B.READ/B.READ/Sources/Util/Extensions/StringProtocol+.swift
deleted file mode 100644
index 7760d481..00000000
--- a/B.READ/B.READ/Sources/Util/Extensions/StringProtocol+.swift
+++ /dev/null
@@ -1,107 +0,0 @@
-//
-// StringProtocol.swift
-// B.READ
-//
-// Created by 심근웅 on 6/8/25.
-//
-
-import Foundation
-import SwiftUI
-
-extension StringProtocol {
- /// 키워드가 포함되는 Index 범위를 찾습니다.
- ///
- /// - Parameter keyword: String중 찾을 키워드
- /// - Returns: 키워드가 일치하는 Index범위 배열
- func allRanges(of keyword: String) -> [Range] {
- guard !keyword.isEmpty else { return [] }
-
- var ranges: [Range] = []
- var start = startIndex
-
- while start < endIndex {
- // 현재 위치부터 키워드 길이만큼의 범위 설정
- guard let end = index(start, offsetBy: keyword.count, limitedBy: endIndex) else {
- break
- }
- let candidate = self[start.. Text {
- // 1. 키워드가 비어있으면 원본문자열을 반환
- guard !keyword.isEmpty else {
- return Text(String(self))
- .font(regularFont)
- .foregroundColor(.black)
- }
-
- // 대소문자 구분하지 않음
- let keyword = keyword.lowercased()
- // 2. 모든 일치하는 범위를 찾음 (겹치는 범위도 포함)
- let ranges = self.allRanges(of: keyword)
-
- // 3. 일치하는 부분이 없으면 원본문자열 반환
- guard !ranges.isEmpty else {
- return Text(String(self))
- .font(regularFont)
- .foregroundColor(.black)
- }
-
- var result = Text("")
- var currentIndex = self.startIndex
-
- // 4. 각 범위를 순회하며 하이라이트 적용
- for range in ranges {
- // 4-1. 일반 텍스트
- if currentIndex < range.lowerBound {
- result = result + Text(String(self[currentIndex..
+
+
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+ UIAppFonts
+
+ Pretendard-Bold.otf
+ Pretendard-Black.otf
+ Pretendard-ExtraBold.otf
+ Pretendard-ExtraLight.otf
+ Pretendard-Light.otf
+ Pretendard-Medium.otf
+ Pretendard-Regular.otf
+ Pretendard-SemiBold.otf
+ Pretendard-Thin.otf
+ PeaceSans.otf
+
+
+
diff --git a/B.READ/RandomSentenceWidget/RandomSentenceWidget.swift b/B.READ/RandomSentenceWidget/RandomSentenceWidget.swift
new file mode 100644
index 00000000..282730cd
--- /dev/null
+++ b/B.READ/RandomSentenceWidget/RandomSentenceWidget.swift
@@ -0,0 +1,108 @@
+//
+// RandomSentenceWidget.swift
+// RandomSentenceWidget
+//
+// Created by 도민준 on 6/9/25.
+//
+
+import WidgetKit
+import SwiftUI
+
+// MARK: - Entry
+struct QuoteEntry: TimelineEntry {
+ let date: Date // 타임라인 기준 시각
+ let quote: String // 인용 문장
+ let bookTitle: String // 책 제목(없으면 "")
+}
+
+// MARK: - Provider
+struct QuoteProvider: TimelineProvider {
+
+ func placeholder(in context: Context) -> QuoteEntry {
+ QuoteEntry(date: .now,
+ quote: "책 속의 한 문장을 표시합니다.",
+ bookTitle: "Placeholder Book")
+ }
+
+ func getSnapshot(in context: Context,
+ completion: @escaping (QuoteEntry) -> Void) {
+ completion(randomEntry())
+ }
+
+ func getTimeline(in context: Context,
+ completion: @escaping (Timeline) -> Void) {
+
+ let entry = randomEntry()
+ let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: entry.date)!
+
+ completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
+ }
+
+ // SharedQuotesStore → 랜덤 추출
+ private func randomEntry() -> QuoteEntry {
+ let picked = SharedQuotesStore.load().randomElement()
+ return QuoteEntry(
+ date: .now,
+ quote: picked?.content ?? "저장된 문장이 없습니다.",
+ bookTitle: picked?.bookTitle ?? ""
+ )
+ }
+}
+
+
+
+struct RandomSentenceWidgetEntryView : View {
+ var entry: QuoteEntry
+
+ var body: some View {
+ HStack(spacing: 12) {
+ Image("HappyBread")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 64, height: 64)
+
+ VStack {
+ Text(entry.quote)
+ .brStyleFont(.pretendard(.semiBold, size: 18), lineHeight: 1.2)
+ .lineLimit(4)
+ .multilineTextAlignment(.trailing)
+ .minimumScaleFactor(0.6)
+ .frame(maxWidth: .infinity, alignment: .trailing)
+
+ if !entry.bookTitle.isEmpty {
+ Text("– \(entry.bookTitle) –")
+ .brStyleFont(.pretendard(.regular, size: 14), lineHeight: 1)
+ .frame(maxWidth: .infinity, alignment: .trailing)
+ }
+ }
+ }
+ .padding()
+ }
+}
+
+struct RandomSentenceWidget: Widget {
+ let kind: String = "RandomSentenceWidget"
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: kind, provider: QuoteProvider()) { entry in
+ if #available(iOS 17.0, *) {
+ RandomSentenceWidgetEntryView(entry: entry)
+ .containerBackground(.fill.tertiary, for: .widget)
+ } else {
+ RandomSentenceWidgetEntryView(entry: entry)
+ .padding()
+ .background(.backgroundDefault)
+ }
+ }
+ .configurationDisplayName("My Widget")
+ .description("This is an example widget.")
+ }
+}
+
+
+#Preview(as: .systemMedium) {
+ RandomSentenceWidget()
+} timeline: {
+ QuoteEntry(date: .now, quote: "위젯 미리보기입니다.", bookTitle: "도서1")
+ QuoteEntry(date: .now, quote: "또 다른 문장 미리보기.", bookTitle: "도서2")
+}
diff --git a/B.READ/RandomSentenceWidget/RandomSentenceWidgetBundle.swift b/B.READ/RandomSentenceWidget/RandomSentenceWidgetBundle.swift
new file mode 100644
index 00000000..147ef3a5
--- /dev/null
+++ b/B.READ/RandomSentenceWidget/RandomSentenceWidgetBundle.swift
@@ -0,0 +1,16 @@
+//
+// RandomSentenceWidgetBundle.swift
+// RandomSentenceWidget
+//
+// Created by 도민준 on 6/9/25.
+//
+
+import WidgetKit
+import SwiftUI
+
+@main
+struct RandomSentenceWidgetBundle: WidgetBundle {
+ var body: some Widget {
+ RandomSentenceWidget()
+ }
+}
diff --git a/B.READ/RandomSentenceWidgetExtension.entitlements b/B.READ/RandomSentenceWidgetExtension.entitlements
new file mode 100644
index 00000000..014e51e9
--- /dev/null
+++ b/B.READ/RandomSentenceWidgetExtension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.BREAD
+
+
+
diff --git a/B.READ/RepositoryTest/RecordRepositoryTest.swift b/B.READ/RepositoryTest/RecordRepositoryTest.swift
index 82b95ab5..269029df 100644
--- a/B.READ/RepositoryTest/RecordRepositoryTest.swift
+++ b/B.READ/RepositoryTest/RecordRepositoryTest.swift
@@ -80,24 +80,6 @@ struct RecordRepositoryTest {
#expect(fetchedRecord == predictResult)
}
- @Test("Recent Finished Record without Summary Fetch Test")
- func fetchRecordAvailableForSummary() async throws {
- let record = DummyData.dummyRecords[2]
-
- try await recordRepository.createRecord(record)
-
- let fetchedRecord = try await recordRepository.fetchRecordAvailableForSummary()
-
- #expect(fetchedRecord == record)
- }
-
- @Test("Recent Finished Record without Summary Fetch Test - Data Not Found")
- func fetchRecordAvailableForSummaryDataNotFound() async throws {
- await #expect(throws: RepositoryError.dataNotFound, performing: {
- try await recordRepository.fetchRecordAvailableForSummary()
- })
- }
-
@Test("Record Delete Test")
func deleteRecord() async throws {
// 1. 넣어둘 레코드
diff --git a/B.READ/RepositoryTest/SummaryRepositoryTest.swift b/B.READ/RepositoryTest/SummaryRepositoryTest.swift
deleted file mode 100644
index 33064447..00000000
--- a/B.READ/RepositoryTest/SummaryRepositoryTest.swift
+++ /dev/null
@@ -1,104 +0,0 @@
-////
-//// SummaryRepositoryTest.swift
-//// RepositoryTest
-////
-//// Created by 김도연 on 6/9/25.
-////
-//
-//import Foundation
-//import Testing
-//
-//@testable import B_READ
-//
-//struct SummaryRepositoryTest {
-//
-// private let recordRepository: RecordRepository
-// private let summaryRepository: SummaryRepository
-//
-// init() {
-// let storage = SwiftDataTestStorage()
-// self.recordRepository = RecordRepositoryImpl(modelContainer: storage.modelContainer)
-// self.summaryRepository = SummaryRepositoryImpl(modelContainer: storage.modelContainer)
-// }
-//
-// @Test("Summary Create Test")
-// func createSummary() async throws {
-// // Given
-// let dummyRecord = DummyData.dummyRecords.first!
-// try await recordRepository.createRecord(dummyRecord)
-// let targetRecord = try await recordRepository.fetchRecord(id: dummyRecord.id)
-// let dummySummary = DummyData.summary1
-//
-// // When
-// try await summaryRepository.createSummary(dummySummary, in: targetRecord)
-// let fetchedSummary = try await summaryRepository.fetchSummary(id: dummySummary.id)
-//
-// // Then
-// #expect(fetchedSummary.id == dummySummary.id)
-// #expect(fetchedSummary.tags.count == dummySummary.tags.count)
-// }
-//
-// @Test("Summary Create Error Test - Data Already Exists")
-// func createSummaryDataAlreadyExists() async throws {
-// // Given
-// let dummyRecord = DummyData.dummyRecords.first!
-// try await recordRepository.createRecord(dummyRecord)
-// let targetRecord = try await recordRepository.fetchRecord(id: dummyRecord.id)
-// let dummySummary = DummyData.summary1
-// try await summaryRepository.createSummary(dummySummary, in: targetRecord)
-//
-// // When + Then
-// await #expect(throws: RepositoryError.dataAlreadyExist, performing: {
-// try await summaryRepository.createSummary(dummySummary, in: targetRecord)
-// })
-// }
-//
-// @Test("Summary Fetch Error Test - Data Not Found")
-// func fetchSummaryDataNotFound() async throws {
-// // When + Then
-// await #expect(throws: RepositoryError.dataNotFound, performing: {
-// _ = try await summaryRepository.fetchSummary(id: "non-existent-id")
-// })
-// }
-//
-// @Test("Summary Fetch All Test")
-// func fetchAllSummaries() async throws {
-// // Given
-// for record in DummyData.dummyRecords {
-// try await recordRepository.createRecord(record)
-// }
-//
-// for i in 0.. $1.createdAt }
-// let actualSummaries = fetchedSummaries.sorted { $0.createdAt > $1.createdAt }
-// #expect(expectedSummaries.first?.id == actualSummaries.first?.id)
-// #expect(expectedSummaries.count == actualSummaries.count)
-// }
-//
-//}
-//
-//// MARK: - Entity Extensions
-//extension AlanSummary: Equatable {
-// public static func == (lhs: AlanSummary, rhs: AlanSummary) -> Bool {
-// return lhs.id == rhs.id &&
-// lhs.isbn == rhs.isbn &&
-// lhs.content == rhs.content &&
-// lhs.tags == rhs.tags &&
-// lhs.createdAt == rhs.createdAt
-// }
-//}
-//
-//extension Tag: Equatable {
-// public static func == (lhs: Tag, rhs: Tag) -> Bool {
-// return lhs.id == rhs.id &&
-// lhs.content == rhs.content
-// }
-//}
diff --git a/B.READ/UsecaseTest/LibraryUseCaseTest.swift b/B.READ/UsecaseTest/LibraryUseCaseTest.swift
index efa2f3a9..9ef4e2c0 100644
--- a/B.READ/UsecaseTest/LibraryUseCaseTest.swift
+++ b/B.READ/UsecaseTest/LibraryUseCaseTest.swift
@@ -14,7 +14,6 @@ struct LibraryUseCaseTest {
private let libraryUseCase: LibraryUseCase
- private let userInfoRepository: UserInfoRepository
private let recordRepository: RecordRepository
private let bookRepository: BookRepository
private let quoteRepository: QuoteRepository
@@ -23,15 +22,13 @@ struct LibraryUseCaseTest {
init() {
let storage = SwiftDataTestStorage()
- 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,
+ bookRepository: bookRepository ,
recordRepository: recordRepository,
quoteRepository: quoteRepository,
bookService: bookService
diff --git a/B.READ/UsecaseTest/MemoUseCaseTest.swift b/B.READ/UsecaseTest/MemoUseCaseTest.swift
index 9b519048..f91a780b 100644
--- a/B.READ/UsecaseTest/MemoUseCaseTest.swift
+++ b/B.READ/UsecaseTest/MemoUseCaseTest.swift
@@ -14,18 +14,15 @@ struct MemoUseCaseTest {
let memoUseCase: MemoUseCase
let recordRepository: RecordRepository
- let userInfoRepository: UserInfoRepository
let bookRepository: BookRepository
let memoRepository: MemoRepository
init() {
let storage = SwiftDataTestStorage()
self.recordRepository = RecordRepositoryImpl(modelContainer: storage.modelContainer)
- self.userInfoRepository = UserInfoRepositoryImpl(modelContainer: storage.modelContainer)
self.bookRepository = BookRepositoryImpl(modelContainer: storage.modelContainer)
self.memoRepository = MemoRepositoryImpl(modelContainer: storage.modelContainer)
self.memoUseCase = MemoUseCaseImpl(
- userInfoRepository: userInfoRepository,
bookRepository: BookRepositoryImpl(modelContainer: storage.modelContainer),
memoRepository: memoRepository,
aiService: AlanService()
diff --git a/B.READ/UsecaseTest/QuoteUseCaseTest.swift b/B.READ/UsecaseTest/QuoteUseCaseTest.swift
index 560d7efb..5bfc4f20 100644
--- a/B.READ/UsecaseTest/QuoteUseCaseTest.swift
+++ b/B.READ/UsecaseTest/QuoteUseCaseTest.swift
@@ -9,127 +9,193 @@ import Foundation
import Testing
@testable import B_READ
-
-struct QuoteUseCaseTest {
-
- private let quoteUseCase: QuoteUseCase
- private let userInfoRepository: UserInfoRepository
- private let bookRepository: BookRepository
- private let recordRepository: RecordRepository
- private let quoteRepository: QuoteRepository
-
- init() {
- let storage = SwiftDataTestStorage()
- self.userInfoRepository = UserInfoRepositoryImpl(modelContainer: storage.modelContainer)
- self.bookRepository = BookRepositoryImpl(modelContainer: storage.modelContainer)
- self.recordRepository = RecordRepositoryImpl(modelContainer: storage.modelContainer)
- self.quoteRepository = QuoteRepositoryImpl(modelContainer: storage.modelContainer)
-
- self.quoteUseCase = QuoteUseCaseImpl(
- userInfoRepository: userInfoRepository,
- quoteRepository: quoteRepository,
- bookRepository: bookRepository
- )
- }
-
- @Test("Quote Create & Id Fetch Test")
- func saveQuoteTestCreate() async throws {
- let dummyRecord = DummyData.dummyRecords[1]
- let dummyQuote = DummyData.dummyQuote[0]
-
- // 1. 레코드와 문장을 각각 생성
- try await recordRepository.createRecord(dummyRecord)
- try await quoteUseCase.saveQuote(dummyQuote, in: dummyRecord)
-
- // 2. 레코드 패치
- let fetchedRecord = try await recordRepository.fetchRecord(id: dummyRecord.id)
-
- // 3. 문장 패치
- let fetchedQuote = try await quoteUseCase.fetchQuote(id: dummyQuote.id)
-
- // 4. 레코드의 문장과 패치한 문장이 같은지 비교
- #expect(fetchedQuote == fetchedRecord.quotes.first)
- }
-
- @Test("Quote Update Test")
- func saveQuoteTestUpdate() async throws {
- let dummyRecord = DummyData.dummyRecords[1]
- var dummyQuote = DummyData.dummyQuote[0]
-
- // 1. 레코드와 문장을 각각 생성
- try await recordRepository.createRecord(dummyRecord)
- try await quoteUseCase.saveQuote(dummyQuote, in: dummyRecord)
-
- // 2. 생성한 문장을 수정
- dummyQuote.content = "수정된 문장입니다."
- dummyQuote.page = 100
-
- // 3. 수정한 문장을 업데이트
- try await quoteUseCase.saveQuote(dummyQuote, in: dummyRecord)
-
- // 4. 레코드 패치
- let fetchedRecord = try await recordRepository.fetchRecord(id: dummyRecord.id)
-
- // 5. 문장 패치
- let fetchedQuote = try await quoteUseCase.fetchQuote(id: dummyQuote.id)
-
- // 6. 레코드의 문장과 패치한 문장이 같은지 비교
- #expect(fetchedQuote == fetchedRecord.quotes.first)
- }
-
- @Test("Quote Remove & AllCase Fetch Test")
- func removeQuoteTest() async throws {
- let dummyRecord = DummyData.dummyRecords[1]
- let dummyQuote = DummyData.dummyQuote[0]
-
- // 1. 레코드와 문장을 각각 생성
- try await recordRepository.createRecord(dummyRecord)
- try await quoteUseCase.saveQuote(dummyQuote, in: dummyRecord)
-
- // 2. 문장을 삭제
- try await quoteUseCase.removeQuote(id: dummyQuote.id)
-
- // 3. 정상적인 삭제일 시, id로 패치하면 에러발생 - data not found
- await #expect(throws: RepositoryError.dataNotFound, performing: {
- _ = try await quoteUseCase.fetchQuote(id: dummyQuote.id)
- })
-
- // 4. 정상적인 삭제일 시, all 패치하면 빈배열([])
- let fetchedQuotes1 = try await quoteUseCase.fetchAllQuotes()
- #expect(fetchedQuotes1 == [])
-
- // 5. 정상적인 삭제일 시, isbn으로 패치하면 빈배열([])
- let fetchedQuotes2 = try await quoteUseCase.fetchQuotes(isbn: dummyRecord.isbn)
- #expect(fetchedQuotes2 == [])
-
- // 6. 정상적인 삭제일 시, 레코드의 Quote는 빈배열([])
- let fetchedRecord = try await recordRepository.fetchRecord(id: dummyRecord.id)
- #expect(fetchedRecord.quotes == [])
- }
-
- @Test("Load Book Title Test")
- func loadBookTitleTest() async throws {
-
- let dummyBook = DummyData.dummyBooks[1]
- let dummyRecord = DummyData.dummyRecords[1]
- let dummyQuote = DummyData.dummyQuote[0]
-
- // 1. 책정보, 레코드, 문장을 각각 생성
- try await bookRepository.createBook(dummyBook)
- try await recordRepository.createRecord(dummyRecord)
- try await quoteUseCase.saveQuote(dummyQuote, in: dummyRecord)
-
- // 2. 레코드를 패치한 후 레코드의 책정보 패치
- let fetchedRecord = try await recordRepository.fetchRecord(id: dummyRecord.id)
- let fetchedBookWithRecord = try await bookRepository.fetchBook(isbn: fetchedRecord.isbn)
-
- // 3. 레코드의 문장 정보를 가지고 문장 패치
- let quoteInRecord = fetchedRecord.quotes[0]
- let fetchedQuote = try await quoteUseCase.fetchQuote(id: quoteInRecord.id)
-
- // 4. 패치 해온 문장으로 책 정보 패치
- let fetchedBookWithQuote = try await quoteUseCase.loadBookTitle(fetchedQuote.isbn)
-
- #expect(fetchedBookWithRecord.name == fetchedBookWithQuote)
- }
-}
+//
+//struct QuoteUseCaseTest {
+//
+// private let quoteRepository: QuoteRepository
+// private let bookRepository: BookRepository
+// private let useCase: QuoteUseCase
+//
+// init() {
+// self.quoteRepository = QuoteRepositoryStub()
+// self.bookRepository = BookRepositoryStub()
+// self.useCase = QuoteUseCaseImpl(
+// quoteRepository: quoteRepository,
+// bookRepository: bookRepository
+// )
+// }
+//
+// @Test("Add Quote Success Test")
+// func addQuoteSuccess() async throws {
+// // given: register test books
+// for book in DummyData.books {
+// try await bookRepository.createBook(book)
+// }
+//
+// // when
+// try await useCase.addQuote(DummyData.quote)
+//
+// // then
+// let stored = try await quoteRepository.fetchQuote(id: DummyData.quote.id)
+// #expect(stored == DummyData.quote)
+// }
+//
+// @Test("Add Quote Empty Content Error Test")
+// func addQuoteEmptyContentError() async throws {
+// // given
+// for book in DummyData.books {
+// try await bookRepository.createBook(book)
+// }
+// var q = DummyData.quote
+// q.content = " "
+//
+// // when / then
+// await #expect(throws: QuoteUseCaseError.emptyContent, performing: {
+// try await useCase.addQuote(q)
+// })
+// }
+//
+// @Test("Add Quote Invalid Page Error Test")
+// func addQuoteInvalidPageError() async throws {
+// // given
+// for book in DummyData.books {
+// try await bookRepository.createBook(book)
+// }
+// var q = DummyData.quote
+// let max = DummyData.books
+// .first { $0.isbn == q.isbn }!
+// .totalPages
+// q.page = max + 1
+//
+// // when / then
+// await #expect(throws: QuoteUseCaseError.invalidPage(max: max), performing: {
+// try await useCase.addQuote(q)
+// })
+// }
+//
+// @Test("Update Quote Success Test")
+// func updateQuoteSuccess() async throws {
+// // given: register book and quote
+// for book in DummyData.books {
+// try await bookRepository.createBook(book)
+// }
+// try await quoteRepository.createQuote(DummyData.quote)
+//
+// var updated = DummyData.quote
+// updated.page = 5
+// updated.content = "Updated"
+//
+// // when
+// try await useCase.updateQuote(updated)
+//
+// // then
+// let fetched = try await quoteRepository.fetchQuote(id: updated.id)
+// #expect(fetched == updated)
+// }
+//
+// @Test("Update Quote Empty Content Error Test")
+// func updateQuoteEmptyContentError() async throws {
+// // given
+// for book in DummyData.books {
+// try await bookRepository.createBook(book)
+// }
+// var updated = DummyData.quote
+// updated.content = ""
+//
+// // when / then
+// await #expect(throws: QuoteUseCaseError.emptyContent, performing: {
+// try await useCase.updateQuote(updated)
+// })
+// }
+//
+// @Test("Remove Quote Success Test")
+// func removeQuoteSuccess() async throws {
+// // given
+// try await quoteRepository.createQuote(DummyData.quote)
+//
+// // when
+// try await useCase.removeQuote(id: DummyData.quote.id)
+//
+// // then
+// await #expect(throws: RepositoryError.dataNotFound, performing: {
+// _ = try await quoteRepository.fetchQuote(id: DummyData.quote.id)
+// })
+// }
+//
+// @Test("Fetch Single Quote Test")
+// func fetchSingleQuote() async throws {
+// // given
+// try await quoteRepository.createQuote(DummyData.quote)
+//
+// // when
+// let fetched = try await useCase.fetchQuote(id: DummyData.quote.id)
+//
+// // then
+// #expect(fetched == DummyData.quote)
+// }
+//
+// @Test("Fetch Quotes by ISBN Test")
+// func fetchQuotesByISBN() async throws {
+// // given
+// let other = Quote(
+// id: "id-2",
+// isbn: DummyData.quote.isbn,
+// content: "Other quote",
+// page: 1
+// )
+// try await quoteRepository.createQuote(DummyData.quote)
+// try await quoteRepository.createQuote(other)
+//
+// // when
+// let list = try await useCase.fetchQuotes(isbn: DummyData.quote.isbn)
+//
+// // then
+// #expect(list == [DummyData.quote, other])
+// }
+//
+// @Test("Fetch All Quotes Test")
+// func fetchAllQuotesTest() async throws {
+// // given
+// let another = Quote(
+// id: "id-3",
+// isbn: "X",
+// content: "X",
+// page: 1
+// )
+// try await quoteRepository.createQuote(DummyData.quote)
+// try await quoteRepository.createQuote(another)
+//
+// // when
+// let all = try await useCase.fetchAllQuotes()
+//
+// // then
+// #expect(all.count == 2)
+// }
+//
+// @Test("Validate Page Success Test")
+// func validatePageSuccess() async throws {
+// // given
+// for book in DummyData.books {
+// try await bookRepository.createBook(book)
+// }
+//
+// // when
+// try await useCase.validatePage(5, forISBN: DummyData.quote.isbn)
+// }
+//
+// @Test("Validate Page Invalid Error Test")
+// func validatePageInvalidError() async throws {
+// // given
+// for book in DummyData.books {
+// try await bookRepository.createBook(book)
+// }
+// let max = DummyData.books
+// .first { $0.isbn == DummyData.quote.isbn }!
+// .totalPages
+//
+// // when / then
+// await #expect(throws: QuoteUseCaseError.invalidPage(max: max), performing: {
+// try await useCase.validatePage(max + 1, forISBN: DummyData.quote.isbn)
+// })
+// }
+//}
diff --git a/B.READ/UsecaseTest/SummaryUseCaseTest.swift b/B.READ/UsecaseTest/SummaryUseCaseTest.swift
deleted file mode 100644
index 56d7e0e5..00000000
--- a/B.READ/UsecaseTest/SummaryUseCaseTest.swift
+++ /dev/null
@@ -1,106 +0,0 @@
-////
-//// SummaryUseCaseTest.swift
-//// UsecaseTest
-////
-//// Created by 김도연 on 6/9/25.
-////
-//
-//import Foundation
-//import Testing
-//
-//@testable import B_READ
-//
-//final class AIServiceMock: AIService {
-// func reset() async throws {
-// print("Impl: ", #function)
-// }
-//
-// func request(prompt: String) async throws -> String {
-// return """
-// {
-// "Summary": "헤르만 헤세의 '싯다르타'는 주인공이 수많은 스승과 가르침을 거쳐가며, 결국 자신의 삶을 통해 진리를 깨닫는 과정을 그립니다. 싯다르타는 남의 말이 아닌 체험을 통해 지혜를 얻으며, 강을 바라보며 모든 존재가 연결되어 흐른다는 사실을 깨닫습니다. 이를 통해 나 또한 변화와 흐름을 있는 그대로 받아들이고 싶어졌습니다. 싯다르타의 삶은 고통과 실수의 연속이었지만, 그 모든 것이 깨달음으로 나아가는 길이었음을 보여줍니다. 나 역시 내 방황을 긍정하고 싶어졌습니다.",
-// "feelingTags": ["깨달음", "연결", "변화", "고통", "긍정"]
-// }
-// """
-// }
-//}
-//
-//
-//struct SummaryUseCaseTest {
-// let summaryUseCase: SummaryUseCase
-// let userInfoRepository: UserInfoRepository
-// let recordRepository: RecordRepository
-// let bookRepository: BookRepository
-// let summaryRepositoy: SummaryRepository
-//
-// init() {
-// let storage = SwiftDataTestStorage()
-// self.userInfoRepository = UserInfoRepositoryImpl(modelContainer: storage.modelContainer)
-// self.recordRepository = RecordRepositoryImpl(modelContainer: storage.modelContainer)
-// self.bookRepository = BookRepositoryImpl(modelContainer: storage.modelContainer)
-// self.summaryRepositoy = SummaryRepositoryImpl(modelContainer: storage.modelContainer)
-//
-// self.summaryUseCase = SummaryUseCaseImpl(
-// userInfoRepository: userInfoRepository,
-// summaryRepository: summaryRepositoy,
-// bookRepository: bookRepository,
-// recordRepository: recordRepository,
-// aiService: AIServiceMock()
-// )
-// }
-//
-// @Test("Summary Generate Success")
-// func generateSummarySuccess() async throws {
-// let dummyRecord = DummyData.recordForSummary
-// try await recordRepository.createRecord(dummyRecord)
-// try await bookRepository.createBook(DummyData.bookForSummary)
-//
-// let summary = try await summaryUseCase.generateSummary(in: dummyRecord)
-//
-// #expect(summary.isbn == dummyRecord.isbn)
-// #expect(!summary.content.isEmpty)
-// #expect(summary.tags.count == 5)
-// }
-//
-// @Test("Summary Generate Error - Book Not Found")
-// func generateSummaryBookNotFound() async throws {
-// let dummyRecord = DummyData.recordForSummary
-// try await recordRepository.createRecord(dummyRecord)
-//
-// await #expect(throws: RepositoryError.dataNotFound, performing: {
-// _ = try await summaryUseCase.generateSummary(in: dummyRecord)
-// })
-// }
-//
-// @Test("Summary Generate Exact Match Test")
-// func generateSummary_resultMatchesExpectedSummary() async throws {
-// let dummyRecord = DummyData.recordForSummary
-// try await recordRepository.createRecord(dummyRecord)
-// try await bookRepository.createBook(DummyData.bookForSummary)
-//
-// let summary = try await summaryUseCase.generateSummary(in: dummyRecord)
-//
-// let expected = DummyData.summaryForFetchTest
-//
-// #expect(summary.isbn == expected.isbn)
-// #expect(summary.content == expected.content)
-// #expect(summary.tags == expected.tags)
-// }
-//
-//}
-//
-//extension AlanSummary: Equatable {
-// public static func == (lhs: AlanSummary, rhs: AlanSummary) -> Bool {
-// return lhs.id == rhs.id &&
-// lhs.isbn == rhs.isbn &&
-// lhs.content == rhs.content &&
-// lhs.tags == rhs.tags &&
-// lhs.createdAt == rhs.createdAt
-// }
-//}
-//
-//extension Tag: Equatable {
-// public static func == (lhs: Tag, rhs: Tag) -> Bool {
-// return lhs.content == rhs.content
-// }
-//}