-
Notifications
You must be signed in to change notification settings - Fork 0
[#289] Todo 카테고리를 유저가 CRUD 할 수 있도록 개선한다 #337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
ad53678
feat: 기존 TodoCategory를 SystemTodoCategory로 수정, UserTodoCategory를 생성하고…
opficdev 0360b8c
feat: 카테고리와 색상을 입력하는 프레젠테이션 구성
opficdev c1e40b0
feat: 사용자 커스텀 카테고리는 삭제 가능
opficdev f314b99
ui: 색상 대신 코드를 보여주고, 해당 색상으로 폰트 색 적용
opficdev 7ae6773
chore: 아키텍쳐 특성 상 발생하는 패턴 disable
opficdev a98a0a0
feat: 서비스 카테고리 설정과 사용자 카테고리 설정을 Firestore을 통해 영속성을 지원하도록 구현
opficdev a38d34f
ui: 사용자가 내릴 수 있다는 표시 추가
opficdev 6a120a8
feat: 커스텀 카테고리를 삭제하면 얼럿이 뜨고 확인 버튼을 눌러야 삭제되도록 구현
opficdev 3aa493e
feat: Firestore에서 fetch 시 로딩뷰가 뜨도록 추가
opficdev 328883c
feat: 카테고리를 제거 시 해당 카테고리를 선택한 TODO를 기타 카테고리로 수정하는 Cloud Functions 구현
opficdev b4c1e13
style: 이름이 어려운 메서드명을 직관적으로 수정
opficdev 8f38d90
fix: 커스텀 카테고리가 디코딩 되지 않는 현상 해결
opficdev 1ba4346
feat: TODO 수정 시 커스텀 카테고리도 떠있을 수 있도록 추가
opficdev 10b5d13
fix: 푸시알림 데이터가 시스템 카테고리만 디코딩 가능한 현상 해결
opficdev 7f81c35
feat: TODO의 카테고리를 변경하면 그에 대응되는 푸시 알림의 데이터도 수정되도록 구현
opficdev e08f89b
refactor: 커스텀 카테고리를 id로 관리
opficdev b29a8b5
chore: firebase 업데이트
opficdev 698ab4e
feat: TODO에서 다른 TODO를 참조 시 커스텀 카테고리로 되어 있어도 참조가 가능하도록 구현
opficdev 32652e8
feat: 사용자 카테고리를 수정할 수 있도록 구현
opficdev 02d13d8
feat: 카테고리를 수정해도 최근 수정 섹션에서는 반영되지 않는 현상 해결
opficdev 05289df
feat: 최대 20자 제한
opficdev 400cb4a
feat: 색상 hex 코드를 탭하면 랜덤으로 색상을 뽑도록 구현
opficdev b240e46
fix: oDomain 메서드가 다른 곳에서 resolve되지 않은 DTO와 함께 호출될 경우 잠재적 오류 발생 가능성 해결
opficdev bd8377d
refactor: 공통 로직을 헬퍼로 묶음
opficdev c2d5e0a
refactor: 불필요 case 문 제거
opficdev d8d5918
fix: 사용자 커스텀 카테고리 끼리는 검사하지 않아 추가
opficdev 0c36340
refactor: 각 레이어별 모델 사용처 명확화
opficdev 6a2922e
fix: 언어에 따라 비교값이 달라질 수 있는 문제 해결
opficdev 300f0a6
refactor: TodoCategoryPreferenceItem -> TodoCategoryItem
opficdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // | ||
| // TodoCategoryResponse.swift | ||
| // DevLog | ||
| // | ||
| // Created by opfic on 3/30/26. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| enum TodoCategoryResponse { | ||
| case raw(String) | ||
| case decoded(TodoCategory) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // | ||
| // TodoReferenceResponse.swift | ||
| // DevLog | ||
| // | ||
| // Created by opfic on 3/30/26. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| struct TodoReferenceResponse { | ||
| let id: String | ||
| let number: Int | ||
| let title: String | ||
| let category: TodoCategoryResponse | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,25 +9,30 @@ import Foundation | |
| import Combine | ||
|
|
||
| final class PushNotificationRepositoryImpl: PushNotificationRepository { | ||
| private let service: PushNotificationService | ||
| private let pushNotificationService: PushNotificationService | ||
| private let todoCategoryService: TodoCategoryService | ||
|
|
||
| init(pushNotificationService: PushNotificationService) { | ||
| self.service = pushNotificationService | ||
| init( | ||
| pushNotificationService: PushNotificationService, | ||
| todoCategoryService: TodoCategoryService | ||
| ) { | ||
| self.pushNotificationService = pushNotificationService | ||
| self.todoCategoryService = todoCategoryService | ||
| } | ||
|
|
||
| /// 푸시 알림 On/Off 설정 | ||
| func fetchPushNotificationEnabled() async throws -> Bool { | ||
| return try await service.fetchPushNotificationEnabled() | ||
| return try await pushNotificationService.fetchPushNotificationEnabled() | ||
| } | ||
|
|
||
| /// 푸시 알림 시간 설정 | ||
| func fetchPushNotificationTime() async throws -> DateComponents { | ||
| return try await service.fetchPushNotificationTime() | ||
| return try await pushNotificationService.fetchPushNotificationTime() | ||
| } | ||
|
|
||
| /// 푸시 알림 설정 업데이트 | ||
| func updatePushNotificationSettings(_ settings: PushNotificationSettings) async throws { | ||
| try await service.updatePushNotificationSettings( | ||
| try await pushNotificationService.updatePushNotificationSettings( | ||
| isEnabled: settings.isEnabled, components: settings.scheduledTime | ||
| ) | ||
| } | ||
|
|
@@ -38,35 +43,125 @@ final class PushNotificationRepositoryImpl: PushNotificationRepository { | |
| cursor: PushNotificationCursor? | ||
| ) async throws -> PushNotificationPage { | ||
| let cursorDTO = cursor.map { PushNotificationCursorDTO.fromDomain($0) } | ||
| let response = try await service.requestNotifications(query, cursor: cursorDTO) | ||
| return try response.toDomain() | ||
| async let responseTask = pushNotificationService.requestNotifications(query, cursor: cursorDTO) | ||
| async let preferencesTask = todoCategoryService.fetchPreferences() | ||
|
|
||
| let (response, preferences) = try await (responseTask, preferencesTask) | ||
| return try resolvePage(from: response, with: preferences) | ||
| } | ||
|
|
||
| func observeNotifications( | ||
| _ query: PushNotificationQuery, | ||
| limit: Int | ||
| ) throws -> AnyPublisher<PushNotificationPage, Error> { | ||
| try service.observeNotifications(query, limit: limit) | ||
| .tryMap { try $0.toDomain() } | ||
| let subject = PassthroughSubject<PushNotificationPage, Error>() | ||
| var cancellable: AnyCancellable? | ||
|
|
||
| cancellable = try pushNotificationService.observeNotifications(query, limit: limit) | ||
| .sink( | ||
| receiveCompletion: { completion in | ||
| switch completion { | ||
| case .finished: | ||
| subject.send(completion: .finished) | ||
| case .failure(let error): | ||
| subject.send(completion: .failure(error)) | ||
| } | ||
| }, | ||
| receiveValue: { [weak self] response in | ||
| guard let self else { return } | ||
|
|
||
| Task { | ||
| do { | ||
| let preferences = try await self.todoCategoryService.fetchPreferences() | ||
| let page = try self.resolvePage(from: response, with: preferences) | ||
| subject.send(page) | ||
| } catch { | ||
| subject.send(completion: .failure(error)) | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+70
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
예를 들어, 다음과 같은 헬퍼 메서드를 만들 수 있습니다. private func resolvePage(from response: PushNotificationPageResponse, with preferences: [TodoCategoryPreference]) throws -> PushNotificationPage {
let userTodoCategories: [UserTodoCategory] = preferences.compactMap { preference in
guard case .user(let userTodoCategory) = preference.category else {
return nil
}
return userTodoCategory
}
let responses = try response.items.map {
try resolve($0, userTodoCategories: userTodoCategories)
}
return try PushNotificationPageResponse(
items: responses,
nextCursor: response.nextCursor
).toDomain()
} |
||
| ) | ||
|
|
||
| return subject | ||
| .handleEvents(receiveCancel: { cancellable?.cancel() }) | ||
| .eraseToAnyPublisher() | ||
| } | ||
|
|
||
| func observeUnreadPushCount() throws -> AnyPublisher<Int, Error> { | ||
| try service.observeUnreadPushCount() | ||
| try pushNotificationService.observeUnreadPushCount() | ||
| .eraseToAnyPublisher() | ||
| } | ||
|
|
||
| // 푸시 알림 기록 삭제 | ||
| func deleteNotification(_ notificationID: String) async throws { | ||
| try await service.deleteNotification(notificationID) | ||
| try await pushNotificationService.deleteNotification(notificationID) | ||
| } | ||
|
|
||
| func undoDeleteNotification(_ notificationID: String) async throws { | ||
| try await service.undoDeleteNotification(notificationID) | ||
| try await pushNotificationService.undoDeleteNotification(notificationID) | ||
| } | ||
|
|
||
| // 푸시 알림 읽음/안읽음 토글 | ||
| func toggleNotificationRead(_ todoId: String) async throws { | ||
| try await service.toggleNotificationRead(todoId) | ||
| try await pushNotificationService.toggleNotificationRead(todoId) | ||
| } | ||
| } | ||
|
|
||
| private extension PushNotificationRepositoryImpl { | ||
| func resolvePage( | ||
| from response: PushNotificationPageResponse, | ||
| with preferences: [TodoCategoryPreference] | ||
| ) throws -> PushNotificationPage { | ||
| let userTodoCategories: [UserTodoCategory] = preferences.compactMap { preference in | ||
| guard case .user(let userTodoCategory) = preference.category else { | ||
| return nil | ||
| } | ||
|
|
||
| return userTodoCategory | ||
| } | ||
|
|
||
| let responses = try response.items.map { | ||
| try resolve($0, userTodoCategories: userTodoCategories) | ||
| } | ||
|
|
||
| return try PushNotificationPageResponse( | ||
| items: responses, | ||
| nextCursor: response.nextCursor | ||
| ).toDomain() | ||
| } | ||
|
|
||
| // resolvePage() 메서드에서만 사용됨 | ||
| private func resolve( | ||
| _ response: PushNotificationResponse, | ||
| userTodoCategories: [UserTodoCategory] | ||
| ) throws -> PushNotificationResponse { | ||
| let id: String | ||
| switch response.todoCategory { | ||
| case .raw(let rawValue): | ||
| id = rawValue | ||
| case .decoded: | ||
| return response | ||
| } | ||
|
|
||
| let todoCategory: TodoCategory | ||
| if let systemTodoCategory = SystemTodoCategory(rawValue: id) { | ||
| todoCategory = .system(systemTodoCategory) | ||
| } else if let userTodoCategory = userTodoCategories.first(where: { | ||
| $0.id == id | ||
| }) { | ||
| todoCategory = .user(userTodoCategory) | ||
| } else { | ||
| throw DataError.invalidData("PushNotificationResponse.todoCategory is invalid: \(id)") | ||
| } | ||
|
|
||
| return PushNotificationResponse( | ||
| id: response.id, | ||
| title: response.title, | ||
| body: response.body, | ||
| receivedAt: response.receivedAt, | ||
| isRead: response.isRead, | ||
| todoId: response.todoId, | ||
| todoCategory: .decoded(todoCategory) | ||
| ) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // | ||
| // TodoCategoryRepositoryImpl.swift | ||
| // DevLog | ||
| // | ||
| // Created by opfic on 3/30/26. | ||
| // | ||
|
|
||
| final class TodoCategoryRepositoryImpl: TodoCategoryRepository { | ||
| private let todoCategoryService: TodoCategoryService | ||
|
|
||
| init(todoCategoryService: TodoCategoryService) { | ||
| self.todoCategoryService = todoCategoryService | ||
| } | ||
|
|
||
| func fetchPreferences() async throws -> [TodoCategoryPreference] { | ||
| try await todoCategoryService.fetchPreferences() | ||
| } | ||
|
|
||
| func updatePreferences(_ preferences: [TodoCategoryPreference]) async throws { | ||
| try await todoCategoryService.updatePreferences(preferences) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toDomain()메서드에서.raw케이스를 처리하는 방식이 잠재적인 버그를 유발할 수 있습니다. 현재 코드는 raw value를SystemTodoCategory로만 변환하려고 시도하며, 사용자 정의 카테고리 ID가 들어올 경우invalidData오류를 발생시킵니다.Repository 단에서 미리 카테고리를 resolve하고 있지만, 이
toDomain메서드가 다른 곳에서 resolve되지 않은 DTO와 함께 호출될 경우 예기치 않은 동작을 일으킬 수 있습니다..raw케이스를 받았을 때 오류를 던져서, 항상 Repository에서 카테고리를 resolve한 후에toDomain()을 호출하도록 강제하는 것이 더 안전한 설계일 것 같습니다.PushNotificationMapping.swift파일의toDomain()메서드에도 동일한 문제가 있습니다.