diff --git a/.gitignore b/.gitignore index 975add5..471a599 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,5 @@ JLPTVoca/SwiftData.md JLPTVoca/WordStability.md .github/.DS_Store + +JLPTVoca/.DS_Store diff --git a/JLPTVoca/.DS_Store b/JLPTVoca/.DS_Store index d811d2f..7d1e7eb 100644 Binary files a/JLPTVoca/.DS_Store and b/JLPTVoca/.DS_Store differ diff --git a/JLPTVoca/JLPTVoca.xcodeproj/project.pbxproj b/JLPTVoca/JLPTVoca.xcodeproj/project.pbxproj index 0afd28b..3c87686 100644 --- a/JLPTVoca/JLPTVoca.xcodeproj/project.pbxproj +++ b/JLPTVoca/JLPTVoca.xcodeproj/project.pbxproj @@ -6,8 +6,19 @@ objectVersion = 77; objects = { +/* Begin PBXContainerItemProxy section */ + 216132B72E71D3C300DE17B7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 214FEB582E5E320300C87957 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 214FEB5F2E5E320300C87957; + remoteInfo = JLPTVoca; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 214FEB602E5E320300C87957 /* JLPTVoca.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JLPTVoca.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 216132B32E71D3C300DE17B7 /* JLPTVocaUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JLPTVocaUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -16,6 +27,11 @@ path = JLPTVoca; sourceTree = ""; }; + 216132B42E71D3C300DE17B7 /* JLPTVocaUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = JLPTVocaUITests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -26,6 +42,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 216132B02E71D3C300DE17B7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -33,6 +56,7 @@ isa = PBXGroup; children = ( 214FEB622E5E320300C87957 /* JLPTVoca */, + 216132B42E71D3C300DE17B7 /* JLPTVocaUITests */, 214FEB612E5E320300C87957 /* Products */, ); sourceTree = ""; @@ -41,6 +65,7 @@ isa = PBXGroup; children = ( 214FEB602E5E320300C87957 /* JLPTVoca.app */, + 216132B32E71D3C300DE17B7 /* JLPTVocaUITests.xctest */, ); name = Products; sourceTree = ""; @@ -70,6 +95,29 @@ productReference = 214FEB602E5E320300C87957 /* JLPTVoca.app */; productType = "com.apple.product-type.application"; }; + 216132B22E71D3C300DE17B7 /* JLPTVocaUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 216132B92E71D3C300DE17B7 /* Build configuration list for PBXNativeTarget "JLPTVocaUITests" */; + buildPhases = ( + 216132AF2E71D3C300DE17B7 /* Sources */, + 216132B02E71D3C300DE17B7 /* Frameworks */, + 216132B12E71D3C300DE17B7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 216132B82E71D3C300DE17B7 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 216132B42E71D3C300DE17B7 /* JLPTVocaUITests */, + ); + name = JLPTVocaUITests; + packageProductDependencies = ( + ); + productName = JLPTVocaUITests; + productReference = 216132B32E71D3C300DE17B7 /* JLPTVocaUITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -83,6 +131,10 @@ 214FEB5F2E5E320300C87957 = { CreatedOnToolsVersion = 16.4; }; + 216132B22E71D3C300DE17B7 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 214FEB5F2E5E320300C87957; + }; }; }; buildConfigurationList = 214FEB5B2E5E320300C87957 /* Build configuration list for PBXProject "JLPTVoca" */; @@ -100,6 +152,7 @@ projectRoot = ""; targets = ( 214FEB5F2E5E320300C87957 /* JLPTVoca */, + 216132B22E71D3C300DE17B7 /* JLPTVocaUITests */, ); }; /* End PBXProject section */ @@ -112,6 +165,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 216132B12E71D3C300DE17B7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -122,8 +182,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 216132AF2E71D3C300DE17B7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 216132B82E71D3C300DE17B7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 214FEB5F2E5E320300C87957 /* JLPTVoca */; + targetProxy = 216132B72E71D3C300DE17B7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 214FEB692E5E320400C87957 /* Debug */ = { isa = XCBuildConfiguration; @@ -302,6 +377,42 @@ }; name = Release; }; + 216132BA2E71D3C300DE17B7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3HYFT54PK2; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.JLPTVocaUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/JLPTVoca.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/JLPTVoca"; + }; + name = Debug; + }; + 216132BB2E71D3C300DE17B7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3HYFT54PK2; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.JLPTVocaUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/JLPTVoca.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/JLPTVoca"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -323,6 +434,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 216132B92E71D3C300DE17B7 /* Build configuration list for PBXNativeTarget "JLPTVocaUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 216132BA2E71D3C300DE17B7 /* Debug */, + 216132BB2E71D3C300DE17B7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 214FEB582E5E320300C87957 /* Project object */; diff --git a/JLPTVoca/JLPTVoca/ContentView.swift b/JLPTVoca/JLPTVoca/ContentView.swift index ad0899f..6cb6090 100644 --- a/JLPTVoca/JLPTVoca/ContentView.swift +++ b/JLPTVoca/JLPTVoca/ContentView.swift @@ -31,15 +31,8 @@ struct ContentView: View { } } .environment(wordManager) - .task(id: words) { - wordManager.setup( - context: context, - words: words - ) + .onAppear() { + wordManager.setContext(context: context) } } } - -#Preview { - ContentView() -} diff --git a/JLPTVoca/JLPTVoca/Extensions/Logger+Extension.swift b/JLPTVoca/JLPTVoca/Extensions/Logger+Extension.swift new file mode 100644 index 0000000..2db1659 --- /dev/null +++ b/JLPTVoca/JLPTVoca/Extensions/Logger+Extension.swift @@ -0,0 +1,14 @@ +// +// Logger+Extension.swift +// JLPTVoca +// +// Created by Rama on 9/9/25. +// + +import Foundation +import OSLog + +extension Logger { + private static var subsystem = Bundle.main.bundleIdentifier! + static let word = Logger(subsystem: subsystem, category: "Word") +} diff --git a/JLPTVoca/JLPTVoca/Managers/WordManager.swift b/JLPTVoca/JLPTVoca/Managers/WordManager.swift index 0d88e3e..80aa66c 100644 --- a/JLPTVoca/JLPTVoca/Managers/WordManager.swift +++ b/JLPTVoca/JLPTVoca/Managers/WordManager.swift @@ -1,4 +1,3 @@ - // // WordManager.swift // JLPTVoca @@ -8,43 +7,82 @@ import SwiftUI import SwiftData +import OSLog @Observable final class WordManager { - var allWords: [Word] = [] - var wordDeck: [Word] = [] + var studyStateDeck: [StudyState] = [] private var context: ModelContext? - func setup( - context: ModelContext, - words: [Word] - ) { + func setContext(context: ModelContext) { self.context = context - self.allWords = words } func prepareSession() { - self.wordDeck = allWords + guard context != nil else { return } + + let reviewableStates = fetchReviewableStates() + let newWords = fetchNewWords(limit: 20) //TODO: RawVal 수정, 사용자 학습 목표량으로 수정 + let newWordStates = createStates(for: newWords) + + self.studyStateDeck = (reviewableStates + newWordStates).shuffled() + } + + private func fetchData(with descriptor: FetchDescriptor) -> [T] { + guard let context else { return [] } + + do { + return try context.fetch(descriptor) + } catch { + print("@Log - \(error.localizedDescription)") + return [] + } + } + + private func fetchReviewableStates() -> [StudyState] { + let now = Date() + let predicate = #Predicate { $0.reviewDate <= now } + let descriptor = FetchDescriptor(predicate: predicate) + + return fetchData(with: descriptor) + } + + func fetchNewWords(limit: Int) -> [Word] { + let predicate = #Predicate { $0.state == nil } + var descriptor = FetchDescriptor(predicate: predicate) + descriptor.fetchLimit = limit + + return fetchData(with: descriptor) + } + + private func createStates(for words: [Word]) -> [StudyState] { + guard let context else { return [] } + + return words.map { newWord in + let newState = StudyState(word: newWord) + context.insert(newState) + return newState + } } func onCardSwipe( id: UUID, direction: CardSwipeDirection ) { - guard let swipedWord = allWords.first(where: { $0.id == id }) else { + guard let swipedState = studyStateDeck.first(where: { $0.word.id == id }) else { return } - + if direction == .left { - swipedWord.maturityState += 1 + swipedState.maturityState = min(swipedState.maturityState + 1, 5) } else { - swipedWord.maturityState = 2 + swipedState.maturityState = 1 } - - wordDeck.removeAll { $0.id == id } - - if wordDeck.isEmpty { + updateNextReviewDate(for: swipedState) + studyStateDeck.removeAll { $0.word.id == id } + + if studyStateDeck.isEmpty { do { try context?.save() } catch { @@ -52,4 +90,28 @@ final class WordManager { } } } + + func updateNextReviewDate(for state: StudyState) { + let now = Date() + let secondsInDay: TimeInterval = 86400 + var interval: TimeInterval = 0 + + switch state.maturityState { + case 1: + interval = secondsInDay * 1 + case 2: + interval = secondsInDay * 3 + case 3: + interval = secondsInDay * 7 + case 4: + interval = secondsInDay * 14 + case 5: + interval = secondsInDay * 30 + default: + state.reviewDate = now + return + } + + state.reviewDate = now.addingTimeInterval(interval) + } } diff --git a/JLPTVoca/JLPTVoca/Models/StudyState.swift b/JLPTVoca/JLPTVoca/Models/StudyState.swift new file mode 100644 index 0000000..a5a645f --- /dev/null +++ b/JLPTVoca/JLPTVoca/Models/StudyState.swift @@ -0,0 +1,22 @@ +// +// StudyState.swift +// JLPTVoca +// +// Created by Rama on 9/2/25. +// + +import SwiftUI +import SwiftData + +@Model +final class StudyState { + @Relationship var word: Word + + var maturityState: Int = 0 + var reviewDate: Date = Date() + var isFavorite: Bool = false + + init(word: Word) { + self.word = word + } +} diff --git a/JLPTVoca/JLPTVoca/Models/Word.swift b/JLPTVoca/JLPTVoca/Models/Word.swift index 029980e..49a0ddb 100644 --- a/JLPTVoca/JLPTVoca/Models/Word.swift +++ b/JLPTVoca/JLPTVoca/Models/Word.swift @@ -13,30 +13,24 @@ final class Word: Decodable { var id: UUID var japanese: [RubyText] var korean: String - var isFavorite: Bool var jlptLevel: Int - var maturityState: Int - var nextReviewDate: Date var plainJapanese: String { japanese.map { $0.text }.joined() } + @Relationship(inverse: \StudyState.word) + var state: StudyState? + init( japanese: [RubyText], korean: String, - jlptLevel: Int, - isFavorite: Bool, - maturityState: Int, - nextReviewDate: Date + jlptLevel: Int ) { self.id = UUID() self.japanese = japanese self.korean = korean self.jlptLevel = jlptLevel - self.isFavorite = isFavorite - self.maturityState = maturityState - self.nextReviewDate = nextReviewDate } required init(from decoder: Decoder) throws { @@ -46,14 +40,11 @@ final class Word: Decodable { self.jlptLevel = try container.decode(Int.self, forKey: .jlptLevel) self.id = UUID() - self.isFavorite = false - self.maturityState = 0 - self.nextReviewDate = Date() } } extension Word { - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey { // TODO: 분리 case japanese, korean, jlptLevel } } diff --git a/JLPTVoca/JLPTVoca/Views/DictionaryView.swift b/JLPTVoca/JLPTVoca/Views/DictionaryView.swift index bd49736..2459ee6 100644 --- a/JLPTVoca/JLPTVoca/Views/DictionaryView.swift +++ b/JLPTVoca/JLPTVoca/Views/DictionaryView.swift @@ -12,11 +12,11 @@ struct DictionaryView: View { var body: some View { NavigationStack { - List(wordManager.allWords) { word in + List(wordManager.studyStateDeck) { state in VStack(alignment: .leading) { - Text(word.plainJapanese) + Text(state.word.plainJapanese) .font(.headline) - Text(word.korean) + Text(state.word.korean) .font(.subheadline) .foregroundStyle(.secondary) } diff --git a/JLPTVoca/JLPTVoca/Views/WordStudyView.swift b/JLPTVoca/JLPTVoca/Views/WordStudyView.swift index 30bc262..ee3696a 100644 --- a/JLPTVoca/JLPTVoca/Views/WordStudyView.swift +++ b/JLPTVoca/JLPTVoca/Views/WordStudyView.swift @@ -36,7 +36,7 @@ struct WordStudyView: View { }, message: { Text("7년 연습생 하고 집에 갈래?") //TODO: RawVal 수정 }) - .onChange(of: wordManager.wordDeck) { _, newDeck in + .onChange(of: wordManager.studyStateDeck) { _, newDeck in if newDeck.isEmpty { showCompletionModal = true } @@ -47,11 +47,11 @@ struct WordStudyView: View { extension WordStudyView { private func wordDeckView() -> some View { ZStack { - ForEach(wordManager.wordDeck) { word in + ForEach(wordManager.studyStateDeck) { state in WordCardView( - id: word.id, - japanese: word.plainJapanese, - korean: word.korean + id: state.word.id, + japanese: state.word.plainJapanese, + korean: state.word.korean ) { id, direction in wordManager.onCardSwipe( diff --git a/JLPTVoca/JLPTVocaUITests/JLPTVocaUITests.swift b/JLPTVoca/JLPTVocaUITests/JLPTVocaUITests.swift new file mode 100644 index 0000000..c372d76 --- /dev/null +++ b/JLPTVoca/JLPTVocaUITests/JLPTVocaUITests.swift @@ -0,0 +1,35 @@ +// +// JLPTVocaUITests.swift +// JLPTVocaUITests +// +// Created by Rama on 9/11/25. +// + +import XCTest + +final class JLPTVocaUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/JLPTVoca/context-summary.md b/JLPTVoca/context-summary.md new file mode 100644 index 0000000..2badc1a --- /dev/null +++ b/JLPTVoca/context-summary.md @@ -0,0 +1,18 @@ +# Project Context Summary (As of 2025-09-09 - Pre-Logging) + +#### 1. Project Goal +- iOS 기반의 JLPT 단어 암기 앱 "JLPT Scheduler" 개발. + +#### 2. Core Guidelines +- `ProjectGuideline.md`와 `GEMINI.md`에 명시된 프로젝트 명세와 협업 규칙을 따릅니다. + +#### 3. Key Decisions & Work Done (up to pre-logging) +- **`WordManager.swift` Refactoring**: + - **목표**: 데이터 접근 로직의 추상화 및 복합 책임 분리를 통한 코드 직관성 향상. + - **결과**: 데이터 접근을 제네릭 `fetchData`로 일원화하고, `prepareSession`의 로직을 명확한 책임 단위의 헬퍼 메서드들(`fetchReviewableStates`, `fetchNewWords`, `createStates`)로 분리하여 가독성과 유지보수성을 개선했습니다. +- **`ContentView.swift` Review**: + - **목표**: View Modifier 사용의 의미적 정확성 검토. + - **결과**: 동기적인 `setContext` 호출에는 비동기용 `.task`보다 동기용 `.onAppear`가 더 적합하다고 판단하여 코드를 수정했습니다. + +#### 4. Current Status +- `WordManager` 리팩토링과 `ContentView` 리뷰를 완료했으며, 다음 단계로 Logger 구현을 시작할 준비가 된 상태입니다. diff --git a/JLPTVoca/project-guideline.md b/JLPTVoca/project-guideline.md new file mode 100644 index 0000000..82ca723 --- /dev/null +++ b/JLPTVoca/project-guideline.md @@ -0,0 +1,77 @@ +# 🎯 JLPT Flashcard App: Mission & Core Product + +### 📌 App Overview (App Context) + +* **Project Name (Tentative):** JLPT Scheduler +* **Platform & Language:** iOS (Swift) +* **Core Problem to Solve:** To efficiently convert about 3,000 JLPT N2 vocabulary words into long-term memory within 6 months without forgetting them. +* **Differentiation Strategy:** Focus on an intuitive level system with workload management to keep users motivated, rather than a complex algorithm. + +### 👤 Persona + +* **User:** A student preparing for the JLPT for employment purposes. +* **Learning Goal:** To obtain the **JLPT N2** certification through consistent study over 6+ months. +* **Main Task:** Memorize a set amount of vocabulary (approx. 3,000 words) daily and retain it in long-term memory. +* **Biggest Pain Point:** "I forget words as soon as I memorize them." With limited time, a method is needed to systematically manage and review a vast amount of vocabulary. + +--- + +## 🚀 Mission + +> To help students preparing for the JLPT for employment to convert thousands of words into perfect long-term memory through a scientific and efficient method, ultimately leading to success in their target exam. + +--- + +## 🧠 Learning Algorithm & Product Specification + +> ℹ️ **Note:** The detailed specification for the core learning algorithm is managed in the `WordStability.md` file. + +--- + +## 📱 App UI/UX Breakdown + +### 0. Main Navigation (TabView) +- **Purpose:** Provides the main navigation to move easily between the app's core features (Home, Dictionary, Settings). +- **Components:** + - **First Tab:** `HomeView` (Home Icon) + - **Second Tab:** `DictionaryView` (Book Icon) + - **Third Tab:** `SettingView` (Settings Icon) +- **Navigation Flow:** + - Starting a session from `HomeView` → Navigates to `WordStudyView`. + - Clicking buttons in `DictionaryView` → Navigates to `EntireWordView` or `FavoriteWordView`. + +### 1. OnboardingView +- **Purpose:** To set the user's learning goals upon first launch. +- **Key Features:** Select target exam level (N1-N5), select target exam date (using a DatePicker), set the daily goal for new words. +- **Action:** After entering all information, tapping 'Start' → Navigates to `HomeView`. + +### 2. HomeView (Main Dashboard) +- **Purpose:** To summarize overall learning progress and act as the gateway to start a learning session. +- **Displayed Information:** D-Day counter for the exam, a circular progress bar for the selected level's progress, a toggle to view word counts by maturity level. +- **Actions:** `Start Learning` button → Navigates to `WordStudyView`, change JLPT level (Dropdown), navigate to Dictionary/Settings. + +### 3. WordStudyView (Learning Session Screen) +- **Purpose:** The core screen for memorizing today's words. +- **Displayed Information:** Progress bar for the current session (e.g., 15/73), a flashcard (Front: Japanese, Back: Meaning/Furigana). +- **Actions:** Tap card (to flip), Swipe card (Left: Incorrect / Right: Correct), Bottom buttons (as an alternative to swiping), Favorite button, Back button (shows an alert to confirm ending the session). + +### 4. DictionaryView (Dictionary Menu) +- **Purpose:** Acts as a fork to navigate to either the full word list or the favorite word list. +- **Actions:** + - `[View All Words]` button → Navigates to `EntireWordView`. + - `[View Favorite Words]` button → Navigates to `FavoriteWordView`. + +### 5. EntireWordView (Full Word List) +- **Purpose:** To search and view details for all words. +- **Key Features:** Search field at the top, a scrollable list of all words. +- **Displayed Information:** Each list cell shows: Japanese, Korean, Furigana, Level, Maturity State, Favorite status. +- **Actions:** Search for words, toggle favorite status. + +### 6. FavoriteWordView (Favorite Word List) +- **Purpose:** To view and search only the user's favorited words. +- **Key Features:** Same as `EntireWordView`, but only shows favorited words. +- **Actions:** Search within favorite words, toggle favorite off (which removes it from the list). + +### 7. SettingView +- **Purpose:** To change the app's main settings. +- **Actions:** Adjust daily new word goal, reset all learning progress (with a confirmation alert).