Skip to content

Commit 0b0ab8b

Browse files
committed
feat: TodoDetailFeature 리듀서 추가
1 parent d49b7e9 commit 0b0ab8b

3 files changed

Lines changed: 583 additions & 1 deletion

File tree

Application/DevLogPresentation/Sources/Common/LoadingState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public final class LoadingState {
2626
private var visibleDelayedTargets = Set<AnyHashable>()
2727
private var visibleTargets = Set<AnyHashable>()
2828

29-
init(delay: Duration = .seconds(0.3)) {
29+
nonisolated init(delay: Duration = .seconds(0.3)) {
3030
self.delay = delay
3131
}
3232

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//
2+
// TodoDetailFeature.swift
3+
// DevLogPresentation
4+
//
5+
// Created by opfic on 6/11/26.
6+
//
7+
8+
import ComposableArchitecture
9+
import DevLogDomain
10+
import Foundation
11+
12+
@Reducer
13+
struct TodoDetailFeature {
14+
@ObservableState
15+
struct State: Equatable {
16+
@Presents var alert: AlertState<Never>?
17+
@Presents var sheet: SheetState?
18+
@Presents var fullScreenCover: FullScreenCoverState?
19+
var todoId: String
20+
var showEditButton: Bool
21+
var todo: Todo?
22+
var referenceItems: [Int: TodoReferenceItem] = [:]
23+
var isLoading = false
24+
}
25+
26+
@ObservableState
27+
struct SheetState: Equatable {
28+
var destination: Destination
29+
30+
enum Destination: Equatable {
31+
case info
32+
case todo(TodoIdItem)
33+
}
34+
35+
static let info = Self(destination: .info)
36+
37+
static func todo(_ todoId: TodoIdItem) -> Self {
38+
Self(destination: .todo(todoId))
39+
}
40+
}
41+
42+
@ObservableState
43+
struct FullScreenCoverState: Equatable {
44+
var destination: Destination
45+
46+
enum Destination: Equatable {
47+
case editor
48+
}
49+
50+
static let editor = Self(destination: .editor)
51+
}
52+
53+
enum Action {
54+
case alert(PresentationAction<Never>)
55+
case sheet(PresentationAction<Sheet>)
56+
case fullScreenCover(PresentationAction<Never>)
57+
case onAppear
58+
case fetchFailed
59+
case setSheet(SheetState?)
60+
case setFullScreenCover(FullScreenCoverState?)
61+
case setTodo(Todo)
62+
case setReferenceItems([Int: TodoReferenceItem])
63+
case setLoading(Bool)
64+
65+
enum Sheet: Equatable {
66+
case tapCloseButton
67+
}
68+
}
69+
70+
@Dependency(\.fetchTodoByIdUseCase) var fetchTodoUseCase
71+
@Dependency(\.fetchReferenceItemsUseCase) var fetchReferenceItemsUseCase
72+
private let loadingState = LoadingState()
73+
74+
var body: some ReducerOf<Self> {
75+
Reduce { state, action in
76+
switch action {
77+
case .sheet(.dismiss):
78+
state.sheet = nil
79+
case .alert:
80+
break
81+
case .sheet(.presented(.tapCloseButton)):
82+
state.sheet = nil
83+
case .sheet:
84+
break
85+
case .fullScreenCover(.dismiss):
86+
state.fullScreenCover = nil
87+
case .fullScreenCover:
88+
break
89+
case .onAppear:
90+
return fetchTodoEffect(todoId: state.todoId)
91+
case .fetchFailed:
92+
state.alert = alertState()
93+
case .setSheet(let sheet):
94+
state.sheet = sheet
95+
case .setFullScreenCover(let cover):
96+
state.fullScreenCover = cover
97+
case .setTodo(let todo):
98+
state.todo = todo
99+
state.referenceItems = [:]
100+
return resolveMarkdownEffect(content: todo.content)
101+
case .setReferenceItems(let items):
102+
state.referenceItems = items
103+
case .setLoading(let value):
104+
state.isLoading = value
105+
}
106+
107+
return .none
108+
}
109+
.ifLet(\.$alert, action: \.alert)
110+
}
111+
}
112+
113+
extension DependencyValues {
114+
var fetchTodoByIdUseCase: FetchTodoByIdUseCase {
115+
get { self[FetchTodoByIdUseCaseKey.self] }
116+
set { self[FetchTodoByIdUseCaseKey.self] = newValue }
117+
}
118+
119+
var fetchReferenceItemsUseCase: FetchReferenceItemsUseCase {
120+
get { self[FetchReferenceItemsUseCaseKey.self] }
121+
set { self[FetchReferenceItemsUseCaseKey.self] = newValue }
122+
}
123+
}
124+
125+
private enum FetchTodoByIdUseCaseKey: DependencyKey {
126+
static var liveValue: FetchTodoByIdUseCase {
127+
preconditionFailure("FetchTodoByIdUseCase must be provided.")
128+
}
129+
130+
static var testValue: FetchTodoByIdUseCase {
131+
liveValue
132+
}
133+
}
134+
135+
private enum FetchReferenceItemsUseCaseKey: DependencyKey {
136+
static var liveValue: FetchReferenceItemsUseCase {
137+
preconditionFailure("FetchReferenceItemsUseCase must be provided.")
138+
}
139+
140+
static var testValue: FetchReferenceItemsUseCase {
141+
liveValue
142+
}
143+
}
144+
145+
private extension TodoDetailFeature {
146+
func fetchTodoEffect(todoId: String) -> Effect<Action> {
147+
.run { [fetchTodoUseCase, loadingState] send in
148+
await loadingState.begin(mode: .delayed) { isLoading in
149+
send(.setLoading(isLoading))
150+
}
151+
do {
152+
let todo = try await fetchTodoUseCase.execute(todoId)
153+
await loadingState.end(mode: .delayed) { isLoading in
154+
send(.setLoading(isLoading))
155+
}
156+
await send(.setTodo(todo))
157+
} catch {
158+
await loadingState.end(mode: .delayed) { isLoading in
159+
send(.setLoading(isLoading))
160+
}
161+
await send(.fetchFailed)
162+
}
163+
}
164+
}
165+
166+
func resolveMarkdownEffect(content: String) -> Effect<Action> {
167+
.run { [fetchReferenceItemsUseCase] send in
168+
let numbers = content.todoReferenceNumbers
169+
var referenceItems = [Int: TodoReferenceItem]()
170+
171+
if !numbers.isEmpty {
172+
do {
173+
referenceItems = try await fetchReferenceItemsUseCase.execute(numbers)
174+
.mapValues(TodoReferenceItem.init(from:))
175+
} catch {
176+
referenceItems = [:]
177+
}
178+
}
179+
180+
await send(.setReferenceItems(referenceItems))
181+
}
182+
}
183+
184+
func alertState() -> AlertState<Never> {
185+
AlertState {
186+
TextState(String(localized: "common_error_title"))
187+
} actions: {
188+
ButtonState(role: .cancel) {
189+
TextState(String(localized: "common_close"))
190+
}
191+
} message: {
192+
TextState(String(localized: "common_error_message"))
193+
}
194+
}
195+
}

0 commit comments

Comments
 (0)