Skip to content
Merged
Show file tree
Hide file tree
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 Mar 29, 2026
0360b8c
feat: 카테고리와 색상을 입력하는 프레젠테이션 구성
opficdev Mar 29, 2026
c1e40b0
feat: 사용자 커스텀 카테고리는 삭제 가능
opficdev Mar 30, 2026
f314b99
ui: 색상 대신 코드를 보여주고, 해당 색상으로 폰트 색 적용
opficdev Mar 30, 2026
7ae6773
chore: 아키텍쳐 특성 상 발생하는 패턴 disable
opficdev Mar 30, 2026
a98a0a0
feat: 서비스 카테고리 설정과 사용자 카테고리 설정을 Firestore을 통해 영속성을 지원하도록 구현
opficdev Mar 30, 2026
a38d34f
ui: 사용자가 내릴 수 있다는 표시 추가
opficdev Mar 30, 2026
6a120a8
feat: 커스텀 카테고리를 삭제하면 얼럿이 뜨고 확인 버튼을 눌러야 삭제되도록 구현
opficdev Mar 30, 2026
3aa493e
feat: Firestore에서 fetch 시 로딩뷰가 뜨도록 추가
opficdev Mar 30, 2026
328883c
feat: 카테고리를 제거 시 해당 카테고리를 선택한 TODO를 기타 카테고리로 수정하는 Cloud Functions 구현
opficdev Mar 30, 2026
b4c1e13
style: 이름이 어려운 메서드명을 직관적으로 수정
opficdev Mar 30, 2026
8f38d90
fix: 커스텀 카테고리가 디코딩 되지 않는 현상 해결
opficdev Mar 30, 2026
1ba4346
feat: TODO 수정 시 커스텀 카테고리도 떠있을 수 있도록 추가
opficdev Mar 30, 2026
10b5d13
fix: 푸시알림 데이터가 시스템 카테고리만 디코딩 가능한 현상 해결
opficdev Mar 30, 2026
7f81c35
feat: TODO의 카테고리를 변경하면 그에 대응되는 푸시 알림의 데이터도 수정되도록 구현
opficdev Mar 30, 2026
e08f89b
refactor: 커스텀 카테고리를 id로 관리
opficdev Mar 30, 2026
b29a8b5
chore: firebase 업데이트
opficdev Mar 30, 2026
698ab4e
feat: TODO에서 다른 TODO를 참조 시 커스텀 카테고리로 되어 있어도 참조가 가능하도록 구현
opficdev Mar 30, 2026
32652e8
feat: 사용자 카테고리를 수정할 수 있도록 구현
opficdev Mar 30, 2026
02d13d8
feat: 카테고리를 수정해도 최근 수정 섹션에서는 반영되지 않는 현상 해결
opficdev Mar 30, 2026
05289df
feat: 최대 20자 제한
opficdev Mar 30, 2026
400cb4a
feat: 색상 hex 코드를 탭하면 랜덤으로 색상을 뽑도록 구현
opficdev Mar 30, 2026
b240e46
fix: oDomain 메서드가 다른 곳에서 resolve되지 않은 DTO와 함께 호출될 경우 잠재적 오류 발생 가능성 해결
opficdev Mar 30, 2026
bd8377d
refactor: 공통 로직을 헬퍼로 묶음
opficdev Mar 30, 2026
c2d5e0a
refactor: 불필요 case 문 제거
opficdev Mar 30, 2026
d8d5918
fix: 사용자 커스텀 카테고리 끼리는 검사하지 않아 추가
opficdev Mar 30, 2026
0c36340
refactor: 각 레이어별 모델 사용처 명확화
opficdev Mar 30, 2026
6a2922e
fix: 언어에 따라 비교값이 달라질 수 있는 문제 해결
opficdev Mar 30, 2026
300f0a6
refactor: TodoCategoryPreferenceItem -> TodoCategoryItem
opficdev Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ disabled_rules:
- multiple_closures_with_trailing_closure
- trailing_whitespace
- type_body_length
- cyclomatic_complexity
- function_body_length
14 changes: 12 additions & 2 deletions DevLog/App/Assembler/DataAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ final class DataAssembler: Assembler {

container.register(TodoRepository.self) {
TodoRepositoryImpl(
todoService: container.resolve(TodoService.self)
todoService: container.resolve(TodoService.self),
todoCategoryService: container.resolve(TodoCategoryService.self)
)
}

container.register(TodoCategoryRepository.self) {
TodoCategoryRepositoryImpl(
todoCategoryService: container.resolve(TodoCategoryService.self)
)
}

Expand Down Expand Up @@ -67,7 +74,10 @@ final class DataAssembler: Assembler {
}

container.register(PushNotificationRepository.self) {
PushNotificationRepositoryImpl(pushNotificationService: container.resolve(PushNotificationService.self))
PushNotificationRepositoryImpl(
pushNotificationService: container.resolve(PushNotificationService.self),
todoCategoryService: container.resolve(TodoCategoryService.self)
)
}

container.register(WebPageRepository.self) {
Expand Down
15 changes: 15 additions & 0 deletions DevLog/App/Assembler/DomainAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class DomainAssembler: Assembler {
registerConnectivityUseCases(container)
registerAuthProviderUseCases(container)
registerTodoUseCases(container)
registerTodoCategoryUseCases(container)
registerUserDataUseCases(container)
registerPushNotificationUseCases(container)
registerWebPageUseCases(container)
Expand Down Expand Up @@ -85,6 +86,20 @@ private extension DomainAssembler {
}
}

func registerTodoCategoryUseCases(_ container: DIContainer) {
container.register(FetchTodoCategoryPreferencesUseCase.self) {
FetchTodoCategoryPreferencesUseCaseImpl(
container.resolve(TodoCategoryRepository.self)
)
}

container.register(UpdateTodoCategoryPreferencesUseCase.self) {
UpdateTodoCategoryPreferencesUseCaseImpl(
container.resolve(TodoCategoryRepository.self)
)
}
}

func registerUserDataUseCases(_ container: DIContainer) {
container.register(FetchUserDataUseCase.self) {
FetchUserDataUseCaseImpl(container.resolve(UserDataRepository.self))
Expand Down
4 changes: 4 additions & 0 deletions DevLog/App/Assembler/InfraAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ final class InfraAssembler: Assembler {
TodoService()
}

container.register(TodoCategoryService.self) {
TodoCategoryService()
}

container.register(UserService.self) {
UserService()
}
Expand Down
2 changes: 1 addition & 1 deletion DevLog/Data/DTO/PushNotificationResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ struct PushNotificationResponse {
let receivedAt: Date
let isRead: Bool
let todoId: String
let todoCategory: String
let todoCategory: TodoCategoryResponse
}
13 changes: 13 additions & 0 deletions DevLog/Data/DTO/TodoCategoryResponse.swift
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)
}
4 changes: 2 additions & 2 deletions DevLog/Data/DTO/TodoDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct TodoRequest: Encodable {
let completedAt: Date?
let dueDate: Date?
let tags: [String]
let category: TodoCategory
let category: String
}

struct TodoResponse {
Expand All @@ -36,5 +36,5 @@ struct TodoResponse {
let completedAt: Date?
let dueDate: Date?
let tags: [String]
let category: String
let category: TodoCategoryResponse
}
15 changes: 15 additions & 0 deletions DevLog/Data/DTO/TodoReferenceResponse.swift
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
}
11 changes: 9 additions & 2 deletions DevLog/Data/Mapper/PushNotificationMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@

extension PushNotificationResponse {
func toDomain() throws -> PushNotification {
guard let todoCategory = TodoCategory(rawValue: self.todoCategory) else {
throw DataError.invalidData("PushNotificationResponse.todoCategory is invalid: \(self.todoCategory)")
let todoCategory: TodoCategory

switch self.todoCategory {
case .decoded(let category):
todoCategory = category
case .raw(let category):
throw DataError.invalidData(
"PushNotificationResponse.todoCategory must be resolved before toDomain(): \(category)"
)
}

return PushNotification(
Expand Down
13 changes: 9 additions & 4 deletions DevLog/Data/Mapper/TodoMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ extension TodoRequest {
completedAt: entity.completedAt,
dueDate: entity.dueDate,
tags: entity.tags,
category: entity.category
category: entity.category.storageValue
)
}
}

extension TodoResponse {
func toDomain() throws -> Todo {
guard let category = TodoCategory(rawValue: self.category) else {
throw DataError.invalidData("TodoResponse.category is invalid: \(self.category)")
let todoCategory: TodoCategory

switch category {
case .decoded(let category):
todoCategory = category
case .raw(let category):
throw DataError.invalidData("TodoResponse.category must be resolved before toDomain(): \(category)")
}

return Todo(
Expand All @@ -44,7 +49,7 @@ extension TodoResponse {
completedAt: self.completedAt,
dueDate: self.dueDate,
tags: self.tags,
category: category
category: todoCategory
)
}
}
Expand Down
123 changes: 109 additions & 14 deletions DevLog/Data/Repository/PushNotificationRepositoryImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
Expand All @@ -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))
}
}
}
)

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)
)
}
}
22 changes: 22 additions & 0 deletions DevLog/Data/Repository/TodoCategoryRepositoryImpl.swift
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)
}
}
Loading