Skip to content

Commit 9523615

Browse files
committed
Merge branch 'release/1.7.0'
2 parents f412f32 + 006058b commit 9523615

File tree

106 files changed

+1404
-691
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+1404
-691
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,6 @@ Resources/InfoPlist/Info.plist
7878

7979
### Using tests
8080
Tests/**/*.plist
81+
82+
### Changelog
83+
Changelog/next.md

Changelog/1.7.0.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## New features
2+
- Added to change theme feature.
3+
4+
## Enhancements
5+
- Prevent adding duplecate word.
6+
7+
## Fixed
8+
- Fixed memory leak for view controller.

Changelog/next.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

Project.swift

Lines changed: 55 additions & 123 deletions
Large diffs are not rendered by default.

QA.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,42 @@
11
#Version
2-
1.5.0
2+
1.7.0
33

44
##Common
55
- Localization 적용 확인
66

7-
##WordCheking
7+
## WordCheking
88

99
- 단어 추가 시 리스트에 반영
1010
- 단어 암기 완료 시 리스트에 반영
1111
- 단어 삭제 시 리스트에 반영
1212
- 단어 앞, 뒤 이동 정상 작동
1313
- 마지막 단어 암기 완료 표시 시 '단어 없음' 문구 출력
1414
- 마지막 단어 삭제 시 '단어 없음' 문구 출력
15+
- 중복 단어 추가 시 중복 단어 Toast 메세지 표시 확인
1516

16-
##WordList
17+
## WordList
1718

1819
- 현재 단어 수정 시 암기 화면에 반영
1920
- 현재 단어 삭제 시 암기 화면에 반영
2021
- 현재 단어 암기 완료 표시 시 암기 화면에 반영
2122
- 현재 단어 없을 때 단어 암기중 표시 시 화면에 반영
2223

23-
##WordSearch
24+
## WordAddition
25+
26+
- 단어 추가 시 중복 단어일 때 중복 경고 메세지 표시 확인
27+
- 단어 추가 시 입력된 단어가 없을 때 완료 버튼 비활성화 확인
28+
29+
## WordDetail
30+
31+
- 단어 편집 시 중복 단어일 때 중복 경고 메세지 표시 확인
32+
- 수정 사항이 존재할 때 취소 확인 ActionSheet 표시 확인
33+
34+
## WordSearch
2435

2536
- 단어 검색 이상 여부 확인
2637
- 단어 검색 후 수정 시 리스트&암기 화면에 바로 적용 여부 확인
2738

28-
##Settings
39+
## Settings
2940

3041
- Source language / Translation language 변경시 번역 사이트에 정상 적용 확인
3142
- 구글 드라이브 로그인 여부에 따라 로그아웃 버튼 표시

Resources/iOSSupport/Localization/en.lproj/Localizable.strings

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ all = "All";
3838
there_are_no_words = "There are no words.";
3939
quick_add_word = "Quick add word";
4040
"%@_added_successfully" = "[ %@ ] added successfully";
41+
already_added_word = "Already added word.";
42+
"%@_added_failed" = "[ %@ ] added failed.";
43+
4144
settings = "Settings";
4245
translation_language = "Translation language";
4346

@@ -66,9 +69,16 @@ dailyReminderFooter = "Sends a daily push notification at the time you set.";
6669
general = "General";
6770
haptics = "Haptics";
6871
hapticsSettingsFooterTextWhenHapticsIsOn = "Enable haptics for interactions.";
69-
hapticsSettingsFooterTextWhenHapticsIsOff = "Disable haptics for interactions";
72+
hapticsSettingsFooterTextWhenHapticsIsOff = "Disable haptics for interactions.";
73+
74+
theme = "Theme";
75+
system_mode = "System mode";
76+
light_mode = "Light mode";
77+
dark_mode = "Dark mode";
7078

7179
more_menu = "More menu";
7280
memorize_words = "Memorize words";
7381
next_word = "Next word";
7482
previous_word = "Previous word";
83+
84+
duplicate_word = "Duplicate word.";

Resources/iOSSupport/Localization/ko.lproj/Localizable.strings

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ all = "전체";
3838
there_are_no_words = "단어가 없습니다.";
3939
quick_add_word = "빠른 단어 추가";
4040
"%@_added_successfully" = "[ %@ ] 추가 성공";
41+
already_added_word = "이미 추가된 단어입니다.";
42+
"%@_added_failed" = "[ %@ ] 단어 추가 실패";
4143

4244
settings = "설정";
4345
languages = "언어";
@@ -68,7 +70,14 @@ haptics = "진동";
6870
hapticsSettingsFooterTextWhenHapticsIsOn = "상호 작용에 대한 진동을 사용합니다.";
6971
hapticsSettingsFooterTextWhenHapticsIsOff = "상호 작용에 대한 진동을 사용하지 않습니다.";
7072

73+
theme = "테마";
74+
system_mode = "시스템 설정 모드";
75+
light_mode = "라이트 모드";
76+
dark_mode = "다크 모드";
77+
7178
more_menu = "메뉴 더보기";
7279
memorize_words = "단어 암기";
7380
next_word = "다음 단어";
7481
previous_word = "이전 단어";
82+
83+
duplicate_word = "중복 단어입니다.";

Sources/Domain/Interfaces/Services/LocalNotificationService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ public protocol LocalNotificationService {
2929

3030
/// 매일 알림을 설정한 마지막 시각을 반환합니다.
3131
///
32-
/// /// - throws: 저장된 시각이 없을 때 Error 를 던집니다.
32+
/// - throws: 저장된 시각이 없을 때 Error 를 던집니다.
3333
func getLatestDailyReminderTime() throws -> DateComponents
3434
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// ExternalStoreUseCaseError.swift
3+
// Domain
4+
//
5+
// Created by Jaewon Yun on 2/12/24.
6+
// Copyright © 2024 woin2ee. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum ExternalStoreUseCaseError: Error {
12+
13+
case noCurrentUser
14+
15+
case noPresentingConfiguration
16+
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// NotificationsUseCaseError.swift
3+
// Domain
4+
//
5+
// Created by Jaewon Yun on 2/12/24.
6+
// Copyright © 2024 woin2ee. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum NotificationsUseCaseError: Error {
12+
case noWordsToMemorize
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// UserSettingsUseCaseError.swift
3+
// Domain
4+
//
5+
// Created by Jaewon Yun on 2/12/24.
6+
// Copyright © 2024 woin2ee. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum UserSettingsUseCaseError: Error {
12+
case noPendingDailyReminder
13+
case noNotificationAuthorization
14+
}

Sources/Domain/Interfaces/UseCases/UserSettingsUseCaseProtocol.swift renamed to Sources/Domain/Interfaces/UseCases/UserSettings/UserSettingsUseCaseProtocol.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ public protocol UserSettingsUseCaseProtocol {
2121

2222
func offHaptics() -> Single<Void>
2323

24+
func updateThemeStyle(_ style: ThemeStyle) -> Single<Void>
25+
2426
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// WordUseCaseError.swift
3+
// Domain
4+
//
5+
// Created by Jaewon Yun on 2/12/24.
6+
// Copyright © 2024 woin2ee. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum WordUseCaseError: Error {
12+
13+
/// `saveFailed` 에러가 발생한 이유입니다.
14+
public enum SaveFailureReason {
15+
16+
/// 저장하려는 단어가 이미 암기 완료 상태입니다.
17+
case wordStateInvalid
18+
19+
/// 저장하려는 단어가 중복 단어입니다.
20+
case duplicatedWord(word: String)
21+
22+
}
23+
24+
/// `retrieveFailed` 에러가 발생한 이유입니다.
25+
public enum RetrieveFailureReason {
26+
27+
/// 해당 UUID 와 일치하는 단어가 없습니다.
28+
case uuidInvaild(uuid: UUID)
29+
30+
}
31+
32+
/// 단어 저장 실패
33+
case saveFailed(reason: SaveFailureReason)
34+
35+
/// 단어 검색 실패
36+
case retrieveFailed(reason: RetrieveFailureReason)
37+
38+
/// 현재 암기중인 단어가 없음
39+
case noMemorizingWords
40+
41+
}

Sources/Domain/Interfaces/UseCases/WordUseCaseProtocol.swift renamed to Sources/Domain/Interfaces/UseCases/Word/WordUseCaseProtocol.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import RxSwift
1111
public protocol WordUseCaseProtocol {
1212

1313
/// 새 단어를 추가합니다.
14+
///
15+
/// - Returns: 단어 추가에 성공하면 Next 이벤트를, 어떠한 이유로 인해 실패하면 `WordUseCaseError` 타입의 Error 이벤트를 방출하는 Sequence 를 반환합니다.
1416
func addNewWord(_ word: Word) -> Single<Void>
1517

1618
/// 단어를 삭제합니다.
@@ -42,4 +44,9 @@ public protocol WordUseCaseProtocol {
4244

4345
func getCurrentUnmemorizedWord() -> Single<Word>
4446

47+
/// `word` 파라미터로 전달된 단어가 이미 저장되어 있는 단어인지 검사합니다.
48+
///
49+
/// - Returns: 반환된 Sequence 는 `ture` or `false` 값을 가진 next 이벤트만 방출됩니다. error 이벤트는 방출되지 않습니다.
50+
func isWordDuplicated(_ word: String) -> Single<Bool>
51+
4552
}

Sources/Domain/UseCases/ExternalStoreUseCase.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,3 @@ public final class ExternalStoreUseCase: ExternalStoreUseCaseProtocol {
172172
}
173173

174174
}
175-
176-
enum ExternalStoreUseCaseError: Error {
177-
178-
case noCurrentUser
179-
180-
case noPresentingConfiguration
181-
182-
}

Sources/Domain/UseCases/NotificationsUseCase.swift

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,11 @@
77
//
88

99
import Foundation
10+
import FoundationPlus
1011
import RxSwift
1112
import RxUtility
12-
import Then
1313
import UserNotifications
1414

15-
enum NotificationsUseCaseError: Error {
16-
case noWordsToMemorize
17-
}
18-
1915
final class NotificationsUseCase: NotificationsUseCaseProtocol {
2016

2117
/// Notification request 의 고유 ID
@@ -59,10 +55,9 @@ final class NotificationsUseCase: NotificationsUseCaseProtocol {
5955
let setDailyReminderSequence: Single<Void> = .create { observer in
6056
let unmemorizedWordCount = self.wordRepository.getUnmemorizedList().count
6157

62-
var content: UNMutableNotificationContent = .init().then {
63-
$0.title = DomainString.daily_reminder
64-
$0.sound = .default
65-
}
58+
var content: UNMutableNotificationContent = .init()
59+
content.title = DomainString.daily_reminder
60+
content.sound = .default
6661

6762
if unmemorizedWordCount == 0 {
6863
content.body = DomainString.daily_reminder_body_message_when_no_words_to_memorize

Sources/Domain/UseCases/UserSettingsUseCase.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ import RxRelay
1212
import RxUtility
1313
import Utility
1414

15-
enum UserSettingsUseCaseError: Error {
16-
case noPendingDailyReminder
17-
case noNotificationAuthorization
18-
}
19-
2015
public final class UserSettingsUseCase: UserSettingsUseCaseProtocol {
2116

2217
let userSettingsRepository: UserSettingsRepositoryProtocol
@@ -100,10 +95,20 @@ public final class UserSettingsUseCase: UserSettingsUseCaseProtocol {
10095
translationTargetLocale = .english
10196
}
10297

103-
let userSettings: UserSettings = .init(translationSourceLocale: .english, translationTargetLocale: translationTargetLocale, hapticsIsOn: true) // FIXME: 처음에 Source Locale 설정 가능하게 (현재 .english 고정)
98+
let initialUserSettings: UserSettings = .init(translationSourceLocale: .english, translationTargetLocale: translationTargetLocale, hapticsIsOn: true, themeStyle: .system) // FIXME: 처음에 Source Locale 설정 가능하게 (현재 .english 고정)
99+
100+
return self.userSettingsRepository.saveUserSettings(initialUserSettings)
101+
}
102+
}
104103

105-
return self.userSettingsRepository.saveUserSettings(userSettings)
104+
public func updateThemeStyle(_ style: ThemeStyle) -> Single<Void> {
105+
return userSettingsRepository.getUserSettings()
106+
.map { currentSettings in
107+
var newSettings = currentSettings
108+
newSettings.themeStyle = style
109+
return newSettings
106110
}
111+
.flatMap { self.userSettingsRepository.saveUserSettings($0) }
107112
}
108113

109114
}

Sources/Domain/UseCases/WordUseCase.swift

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ public final class WordUseCase: WordUseCaseProtocol {
2323
public func addNewWord(_ word: Word) -> RxSwift.Single<Void> {
2424
return .create { single in
2525
guard word.memorizedState != .memorized else {
26-
single(.failure(WordUseCaseError.canNotSaveWord(reason: "Can only add word with a memorization state of `.memorizing`.")))
26+
single(.failure(WordUseCaseError.saveFailed(reason: .wordStateInvalid)))
27+
return Disposables.create()
28+
}
29+
30+
let allWords = self.wordRepository.getAllWords()
31+
if allWords.contains(where: { $0.word.lowercased() == word.word.lowercased() }) {
32+
single(.failure(WordUseCaseError.saveFailed(reason: .duplicatedWord(word: word.word))))
2733
return Disposables.create()
2834
}
2935

@@ -85,14 +91,23 @@ public final class WordUseCase: WordUseCaseProtocol {
8591
if let word = self.wordRepository.getWord(by: uuid) {
8692
single(.success(word))
8793
} else {
88-
single(.failure(WordUseCaseError.invalidUUID(uuid)))
94+
single(.failure(WordUseCaseError.retrieveFailed(reason: .uuidInvaild(uuid: uuid))))
8995
}
9096

9197
return Disposables.create()
9298
}
9399
}
94100

95101
public func updateWord(by uuid: UUID, to newWord: Word) -> RxSwift.Single<Void> {
102+
guard let originWord = wordRepository.getWord(by: uuid) else {
103+
return .error(WordUseCaseError.retrieveFailed(reason: .uuidInvaild(uuid: uuid)))
104+
}
105+
106+
let allWords = self.wordRepository.getAllWords()
107+
if (originWord.word != newWord.word) && allWords.contains(where: { $0.word.lowercased() == newWord.word.lowercased() }) {
108+
return .error(WordUseCaseError.saveFailed(reason: .duplicatedWord(word: newWord.word)))
109+
}
110+
96111
return .create { single in
97112
let updateTarget: Word = .init(
98113
uuid: uuid,
@@ -151,7 +166,7 @@ public final class WordUseCase: WordUseCaseProtocol {
151166
public func markCurrentWordAsMemorized(uuid: UUID) -> RxSwift.Single<Void> {
152167
return .create { single in
153168
guard let currentWord = self.wordRepository.getWord(by: uuid) else {
154-
single(.failure(WordUseCaseError.invalidUUID(uuid)))
169+
single(.failure(WordUseCaseError.retrieveFailed(reason: .uuidInvaild(uuid: uuid))))
155170
return Disposables.create()
156171
}
157172

@@ -174,17 +189,13 @@ public final class WordUseCase: WordUseCaseProtocol {
174189
return .just(currentWord)
175190
}
176191

177-
}
178-
179-
enum WordUseCaseError: Error {
180-
181-
/// 해당되는 단어가 없는 UUID
182-
case invalidUUID(UUID)
183-
184-
/// 단어를 저장할 수 없음
185-
case canNotSaveWord(reason: String)
186-
187-
/// 현재 암기중인 단어가 없음
188-
case noMemorizingWords
192+
public func isWordDuplicated(_ word: String) -> Single<Bool> {
193+
let allWords = self.wordRepository.getAllWords()
194+
if allWords.contains(where: { $0.word.lowercased() == word.lowercased() }) {
195+
return .just(true)
196+
} else {
197+
return .just(false)
198+
}
199+
}
189200

190201
}

0 commit comments

Comments
 (0)