Skip to content

Commit d49b7e9

Browse files
authored
[#569] TodoManageView에 TCA를 적용한다 (#587)
* chore: TCA 패키지 버전 범위 조정 * feat: TodoManageView TCA 적용 * refactor: TodoManage -> CategoryManage * refactor: 폴더링 이동 * refactor: 폴더링 수정
1 parent f6b023d commit d49b7e9

17 files changed

Lines changed: 466 additions & 324 deletions
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
//
2+
// CategoryManageFeature.swift
3+
// DevLogPresentation
4+
//
5+
// Created by opfic on 6/11/26.
6+
//
7+
8+
import ComposableArchitecture
9+
import DevLogDomain
10+
import SwiftUI
11+
12+
@Reducer
13+
struct CategoryManageFeature {
14+
@ObservableState
15+
struct State: Equatable {
16+
var preferences: [TodoCategoryItem]
17+
@Presents var categorySheet: CategorySheetState?
18+
@Presents var alert: AlertState<Action.Alert>?
19+
}
20+
21+
@ObservableState
22+
struct CategorySheetState: Equatable {
23+
var category: UserTodoCategory
24+
var preferences: [TodoCategoryItem]
25+
26+
var isEditing: Bool {
27+
preferences.contains { $0.id == category.id }
28+
}
29+
var navigationTitle: String {
30+
isEditing
31+
? String(localized: "todo_manage_edit_category_title")
32+
: String(localized: "todo_manage_add_category_title")
33+
}
34+
var submitTitle: String {
35+
isEditing
36+
? String(localized: "todo_manage_save")
37+
: String(localized: "todo_add")
38+
}
39+
var placeholder: String {
40+
category.name
41+
}
42+
var categoryNameCountText: String {
43+
"\(category.name.count)/20"
44+
}
45+
var canSubmitUserCategory: Bool {
46+
let name = category.name.trimmingCharacters(in: .whitespacesAndNewlines)
47+
if name.isEmpty {
48+
return false
49+
}
50+
51+
if SystemTodoCategory.allCases.contains(where: {
52+
$0.rawValue.caseInsensitiveCompare(name) == .orderedSame
53+
}) {
54+
return false
55+
}
56+
57+
if preferences.contains(where: { item in
58+
guard case .user(let userCategory) = item.category, userCategory.id != category.id else {
59+
return false
60+
}
61+
62+
return userCategory.name.caseInsensitiveCompare(name) == .orderedSame
63+
}) {
64+
return false
65+
}
66+
67+
if let item = preferences.first(where: { $0.id == category.id }) {
68+
if case .user(let originalCategory) = item.category {
69+
let originalName = originalCategory.name.trimmingCharacters(in: .whitespacesAndNewlines)
70+
if originalName == name && originalCategory.colorHex == category.colorHex {
71+
return false
72+
}
73+
}
74+
}
75+
76+
return true
77+
}
78+
var todoCategoryItem: TodoCategoryItem {
79+
TodoCategoryItem(
80+
from: .user(
81+
UserTodoCategory(
82+
id: category.id,
83+
name: category.name.trimmingCharacters(in: .whitespacesAndNewlines),
84+
colorHex: category.colorHex
85+
)
86+
)
87+
)
88+
}
89+
}
90+
91+
enum Action {
92+
case alert(PresentationAction<Alert>)
93+
case categorySheet(PresentationAction<CategorySheet>)
94+
case tapAddUserCategory
95+
case moveItem(from: IndexSet, target: Int)
96+
case tapItem(TodoCategoryItem)
97+
case tapEditUserCategory(TodoCategoryItem)
98+
case tapDeleteUserCategory(TodoCategoryItem)
99+
case tapDoneButton
100+
101+
enum Alert: Equatable {
102+
case confirmDeleteUserCategory(TodoCategoryItem)
103+
}
104+
105+
enum CategorySheet: Equatable {
106+
case setCategoryName(String)
107+
case setCategoryColor(String)
108+
case tapCloseButton
109+
case tapRandomColorButton
110+
case tapSaveButton
111+
}
112+
}
113+
114+
var body: some ReducerOf<Self> {
115+
Reduce { state, action in
116+
switch action {
117+
case .alert(.presented(.confirmDeleteUserCategory(let item))):
118+
if let index = state.preferences.firstIndex(where: { $0.id == item.id }) {
119+
state.preferences.remove(at: index)
120+
}
121+
case .alert:
122+
break
123+
case .categorySheet(.dismiss):
124+
state.categorySheet = nil
125+
case .categorySheet(.presented(.tapCloseButton)):
126+
state.categorySheet = nil
127+
case .categorySheet(.presented(.setCategoryName(let name))):
128+
state.categorySheet?.category.name = String(name.prefix(20))
129+
case .categorySheet(.presented(.setCategoryColor(let colorHex))):
130+
state.categorySheet?.category.colorHex = colorHex
131+
case .categorySheet(.presented(.tapRandomColorButton)):
132+
if let randomHexValue = Color.randomValue.hexValue {
133+
state.categorySheet?.category.colorHex = randomHexValue
134+
}
135+
case .categorySheet(.presented(.tapSaveButton)):
136+
if var item = state.categorySheet?.todoCategoryItem {
137+
if let index = state.preferences.firstIndex(where: { $0.id == item.id }) {
138+
item.isVisible = state.preferences[index].isVisible
139+
state.preferences[index] = item
140+
} else {
141+
state.preferences.append(item)
142+
}
143+
144+
state.categorySheet = nil
145+
}
146+
case .categorySheet:
147+
break
148+
case .tapAddUserCategory:
149+
if let randomHexValue = Color.randomValue.hexValue {
150+
state.categorySheet = CategorySheetState(
151+
category: UserTodoCategory(
152+
id: UUID().uuidString.lowercased(),
153+
name: "",
154+
colorHex: randomHexValue
155+
),
156+
preferences: state.preferences
157+
)
158+
}
159+
case .moveItem(let from, let target):
160+
state.preferences.move(fromOffsets: from, toOffset: target)
161+
case .tapItem(let item):
162+
if let index = state.preferences.firstIndex(where: { $0.id == item.id }) {
163+
state.preferences[index].isVisible.toggle()
164+
}
165+
case .tapEditUserCategory(let item):
166+
if item.isUserCategory, case .user(let category) = item.category {
167+
state.categorySheet = CategorySheetState(
168+
category: category,
169+
preferences: state.preferences
170+
)
171+
}
172+
case .tapDeleteUserCategory(let item):
173+
if item.isUserCategory {
174+
state.alert = deleteAlertState(for: item)
175+
}
176+
case .tapDoneButton:
177+
break
178+
}
179+
return .none
180+
}
181+
.ifLet(\.$alert, action: \.alert)
182+
}
183+
}
184+
185+
private extension CategoryManageFeature {
186+
func deleteAlertState(for item: TodoCategoryItem) -> AlertState<Action.Alert> {
187+
AlertState {
188+
TextState(String(localized: "todo_manage_delete_category_title"))
189+
} actions: {
190+
ButtonState(role: .cancel) {
191+
TextState(String(localized: "common_cancel"))
192+
}
193+
ButtonState(role: .destructive, action: .confirmDeleteUserCategory(item)) {
194+
TextState(String(localized: "common_delete"))
195+
}
196+
} message: {
197+
TextState(String(localized: "todo_manage_delete_category_message"))
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)