Skip to content

Commit 156b913

Browse files
committed
[Feat] #202 - 이동과 복제와 관련된 작업 하위 리듀서들로 캡슐화
1 parent 449d759 commit 156b913

8 files changed

Lines changed: 121 additions & 291 deletions

Neki-iOS/Features/Archive/Sources/Presentation/Sources/Feature/AlbumSelectionFeature.swift

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ struct AlbumSelectionFeature {
1616
var selectedAlbumIDs: Set<Int> = []
1717
var uploadCount: Int
1818
var isFetching: Bool = false
19+
var isLoading: Bool = false
1920

21+
var photoIDs: [Int]
2022
var selectionPurpose: PhotoSelectionPurpose
2123
var currentAlbumId: Int?
2224

@@ -28,8 +30,9 @@ struct AlbumSelectionFeature {
2830
return !newAlbumTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && albumTitleErrorMessage == nil
2931
}
3032

31-
init(uploadCount: Int = 1, selectionPurpose: PhotoSelectionPurpose, currentAlbumId: Int? = nil) {
32-
self.uploadCount = uploadCount
33+
init(photoIDs: [Int], selectionPurpose: PhotoSelectionPurpose, currentAlbumId: Int? = nil) {
34+
self.photoIDs = photoIDs
35+
self.uploadCount = photoIDs.count
3336
self.selectionPurpose = selectionPurpose
3437
self.currentAlbumId = currentAlbumId
3538
}
@@ -48,14 +51,18 @@ struct AlbumSelectionFeature {
4851
case tapAlbum(Int)
4952
case tapConfirm
5053

54+
// 내부 통신 결과 처리
55+
case taskCompleted(message: String)
56+
case taskFailed(message: String)
57+
5158
// 앨범 생성 액션
5259
case onTapCancelAddAlbum
5360
case onTapConfirmAddAlbum
5461
case addFolderResponse(Result<Int, Error>)
5562

5663
case delegate(DelegateAction)
5764
enum DelegateAction {
58-
case didSelectAlbums(albumIds: [Int])
65+
case didCompleteTask(message: String)
5966
case didTapCancel
6067
case showToast(NekiToastItem)
6168
}
@@ -136,9 +143,25 @@ struct AlbumSelectionFeature {
136143

137144
case .tapConfirm:
138145
guard !state.selectedAlbumIDs.isEmpty else { return .none }
139-
return .send(.delegate(.didSelectAlbums(albumIds: Array(state.selectedAlbumIDs))))
146+
state.isLoading = true
147+
let purpose = state.selectionPurpose
148+
149+
return .run { send in
150+
// TODO: 실제 API 연동 (archiveClient.duplicatePhoto / movePhoto)
151+
try? await Task.sleep(for: .seconds(1))
152+
153+
let msg = purpose == .duplicate ? "사진을 앨범에 추가했어요" : "사진을 앨범으로 이동했어요"
154+
await send(.taskCompleted(message: msg))
155+
}
156+
157+
case let .taskCompleted(message):
158+
state.isLoading = false
159+
return .send(.delegate(.didCompleteTask(message: message)))
160+
161+
case let .taskFailed(message):
162+
state.isLoading = false
163+
return .send(.delegate(.showToast(NekiToastItem(message, style: .error))))
140164

141-
// MARK: - 앨범 생성 관련 처리
142165
case .onTapCancelAddAlbum:
143166
state.newAlbumTitle = ""
144167
state.albumTitleErrorMessage = nil
@@ -159,7 +182,7 @@ struct AlbumSelectionFeature {
159182
case .addFolderResponse(.success):
160183
return .merge(
161184
.send(.delegate(.showToast(NekiToastItem("새로운 앨범을 추가했어요", style: .success)))),
162-
.send(.onAppear) // 앨범 목록 갱신
185+
.send(.onAppear)
163186
)
164187

165188
case .addFolderResponse(.failure):

Neki-iOS/Features/Archive/Sources/Presentation/Sources/Feature/ArchiveAlbumDetailFeature.swift

Lines changed: 18 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -64,24 +64,18 @@ struct ArchiveAlbumDetailFeature {
6464
case onTapDuplicateButton
6565
case onTapMoveButton
6666
case albumSelection(PresentationAction<AlbumSelectionFeature.Action>)
67-
case duplicatePhotosResponse(Result<Void, Error>)
68-
case movePhotosResponse(Result<Void, Error>)
6967

7068
case onTapDownloadButton
7169
case downloadImagesResponse(successCount: Int)
7270

73-
// 사진 삭제 액션
7471
case onTapDeleteButton(option: ArchivePhotoDeleteOption)
7572
case deletePhotosResponse(Result<Void, Error>)
7673

77-
// 앨범 삭제 액션
7874
case onTapExecuteDeleteAlbum(option: ArchiveAlbumDeleteOption)
7975
case deleteAlbumResponse(Result<Void, Error>)
8076

81-
// 사진 가져오기 액션
8277
case onTapImportPhotos
8378
case photoImport(PresentationAction<PhotoImportFeature.Action>)
84-
case importPhotosResponse(Result<Void, Error>)
8579

8680
case fetchPhotos
8781
case photoListResponse(Result<[PhotoEntity], Error>)
@@ -107,17 +101,13 @@ struct ArchiveAlbumDetailFeature {
107101
Reduce { state, action in
108102
switch action {
109103
case .onTapBackButton: return .run { _ in await dismiss() }
110-
111104
case .onAppear: return .send(.fetchPhotos)
112-
113105
case .toggleDropDownMenu:
114106
state.showDropDownMenu.toggle()
115107
return .none
116-
117108
case .closeDropDownMenu:
118109
state.showDropDownMenu = false
119110
return .none
120-
121111
case let .onTapFavorite(item):
122112
let newStatus = !item.isFavorite
123113
state.photos[id: item.id]?.isFavorite = newStatus
@@ -129,23 +119,18 @@ struct ArchiveAlbumDetailFeature {
129119
await send(.toggleFavoriteResponse(photoID: id, result: .failure(error)))
130120
}
131121
}
132-
133122
case .toggleFavoriteResponse(_, .success): return .none
134-
135123
case let .toggleFavoriteResponse(photoID, .failure):
136124
state.photos[id: photoID]?.isFavorite.toggle()
137125
return .send(.delegate(.showToast(NekiToastItem("즐겨찾기 변경에 실패했어요", style: .error))))
138-
139126
case let .imagePicker(.delegate(.imagesConverted(entities))):
140127
state.showDropDownMenu = false
141128
return .send(.processUploadImages(entities: entities))
142-
143129
case let .processUploadImages(entities):
144130
state.isLoading = false
145131
guard !entities.isEmpty else { return .none }
146132
state.isLoading = true
147133
return .send(.registerPhotos(entities: entities))
148-
149134
case let .registerPhotos(entities):
150135
let albumId = state.album.id
151136
return .run { send in
@@ -155,43 +140,35 @@ struct ArchiveAlbumDetailFeature {
155140
try await archiveClient.registerPhotos(folderId: albumId, uploads: uploads ,favorite: false)
156141
}))
157142
}
158-
159143
case .registerPhotosResponse(.success):
160144
state.isLoading = false
161145
return .run { send in
162146
await send(.delegate(.showToast(NekiToastItem("이미지를 추가했어요", style: .success))))
163147
await send(.fetchPhotos)
164148
}
165-
166149
case .registerPhotosResponse(.failure):
167150
state.isLoading = false
168151
return .send(.delegate(.showToast(NekiToastItem("업로드에 실패했어요", style: .error))))
169-
170152
case .onTapCancelEditAlbum:
171153
state.newAlbumTitle = state.album.title
172154
state.albumTitleErrorMessage = nil
173155
return .none
174-
175156
case .onTapConfirmEditAlbum:
176157
guard state.isConfirmButtonEnabled else { return .none }
177158
let title = state.newAlbumTitle.trimmingCharacters(in: .whitespacesAndNewlines)
178159
state.albumTitleErrorMessage = nil
179160
return .run { [albumId = state.album.id] send in
180161
await send(.editAlbumResponse(Result { try await archiveClient.editAlbumName(albumId, title) }))
181162
}
182-
183163
case .editAlbumResponse(.success): return .send(.delegate(.showToast(NekiToastItem("앨범 이름을 변경했어요", style: .success))))
184-
185164
case .editAlbumResponse(.failure):
186165
state.newAlbumTitle = state.album.title
187166
return .send(.delegate(.showToast(NekiToastItem("앨범 이름을 변경하지 못했어요", style: .error))))
188-
189167
case .fetchPhotos:
190168
state.isFetchingPhotos = true
191169
return .run { [albumId = state.album.id] send in
192170
await send(.photoListResponse(Result { try await archiveClient.fetchPhotoList(folderId: albumId, size: 20, sortOrder: nil) }))
193171
}
194-
195172
case let .photoListResponse(.success(entities)):
196173
state.isFetchingPhotos = false
197174
let currentAlbumId = state.album.id
@@ -200,110 +177,60 @@ struct ArchiveAlbumDetailFeature {
200177
}
201178
state.photos = IdentifiedArray(uniqueElements: newItems)
202179
return .none
203-
204180
case .photoListResponse(.failure):
205181
state.isFetchingPhotos = false
206182
return .send(.delegate(.showToast(NekiToastItem("사진을 불러오지 못했어요", style: .error))))
207-
208183
case .loadMorePhotos: return .send(.fetchPhotos)
209-
210184
case .onTapSelectButton:
211185
state.showDropDownMenu = false
212186
state.isSelectionMode = true
213187
return .none
214-
215188
case .onTapCancelSelectButton:
216189
state.isSelectionMode = false
217190
state.selectedIDs.removeAll()
218191
return .none
219-
220192
case let .imageTapped(item):
221193
if state.isSelectionMode {
222194
if state.selectedIDs.contains(item.id) { state.selectedIDs.remove(item.id) }
223195
else { state.selectedIDs.insert(item.id) }
224196
}
225197
return .none
226-
227198
case .onTapDuplicateButton:
228-
state.selectionPurpose = .duplicate
229-
state.albumSelection = AlbumSelectionFeature.State(uploadCount: state.selectedIDs.count, selectionPurpose: .duplicate, currentAlbumId: state.album.id)
199+
state.albumSelection = AlbumSelectionFeature.State(photoIDs: Array(state.selectedIDs), selectionPurpose: .duplicate, currentAlbumId: state.album.id)
230200
return .none
231-
232201
case .onTapMoveButton:
233-
state.selectionPurpose = .move
234-
state.albumSelection = AlbumSelectionFeature.State(uploadCount: state.selectedIDs.count, selectionPurpose: .move, currentAlbumId: state.album.id)
202+
state.albumSelection = AlbumSelectionFeature.State(photoIDs: Array(state.selectedIDs), selectionPurpose: .move, currentAlbumId: state.album.id)
235203
return .none
236-
237204
case let .albumSelection(.presented(.delegate(delegateAction))):
238205
switch delegateAction {
239-
case let .didSelectAlbums(albumIds):
240-
let purpose = state.selectionPurpose
241-
206+
case let .didCompleteTask(message):
242207
state.albumSelection = nil
243-
state.selectionPurpose = nil
244-
state.isLoading = true
245-
246-
return .run { send in
247-
if purpose == .duplicate {
248-
// TODO: API 붙이기
249-
try? await Task.sleep(for: .seconds(1)) // 테스트용 임시 딜레이
250-
await send(.duplicatePhotosResponse(.success(())))
251-
} else {
252-
// TODO: API 붙이기
253-
try? await Task.sleep(for: .seconds(1)) // 테스트용 임시 딜레이
254-
await send(.movePhotosResponse(.success(())))
255-
}
256-
}
257-
208+
state.isSelectionMode = false
209+
state.selectedIDs.removeAll()
210+
return .merge(
211+
.send(.delegate(.showToast(NekiToastItem(message, style: .success)))),
212+
.send(.fetchPhotos)
213+
)
258214
case .didTapCancel:
259215
state.albumSelection = nil
260-
state.selectionPurpose = nil
261216
return .none
262-
263217
case let .showToast(toastItem):
264218
return .send(.delegate(.showToast(toastItem)))
265219
}
266-
267-
case .duplicatePhotosResponse(.success):
268-
state.isLoading = false
269-
state.isSelectionMode = false
270-
state.selectedIDs.removeAll()
271-
return .send(.delegate(.showToast(NekiToastItem("사진을 앨범에 추가했어요", style: .success))))
272-
273-
case .duplicatePhotosResponse(.failure):
274-
state.isLoading = false
275-
return .send(.delegate(.showToast(NekiToastItem("사진 추가에 실패했어요", style: .error))))
276-
277-
case .movePhotosResponse(.success):
278-
state.isLoading = false
279-
state.isSelectionMode = false
280-
state.selectedIDs.removeAll()
281-
return .merge(
282-
.send(.delegate(.showToast(NekiToastItem("사진을 앨범으로 이동했어요", style: .success)))),
283-
.send(.fetchPhotos)
284-
)
285-
286-
case .movePhotosResponse(.failure):
287-
state.isLoading = false
288-
return .send(.delegate(.showToast(NekiToastItem("사진 이동에 실패했어요", style: .error))))
289-
290220
case .onTapDownloadButton:
291221
guard !state.selectedIDs.isEmpty else { return .none }
292222
state.isLoading = true
293223
return .run { [photos = state.photos, selectedIDs = state.selectedIDs] send in
294224
let urls = selectedIDs.compactMap { photos[id: $0]?.imageURL }
295-
296225
let count = try await imageDownloadClient.downloadImages(urls: urls)
297226
await send(.downloadImagesResponse(successCount: count))
298227
}
299-
300228
case let .downloadImagesResponse(count):
301229
state.isLoading = false
302230
state.isSelectionMode = false
303231
state.selectedIDs.removeAll()
304232
if count > 0 { return .send(.delegate(.showToast(NekiToastItem("사진을 갤러리에 다운로드했어요", style: .success)))) }
305233
else { return .send(.delegate(.showToast(NekiToastItem("사진 저장에 실패했어요", style: .error)))) }
306-
307234
case let .onTapDeleteButton(option):
308235
guard !state.selectedIDs.isEmpty else { return .none }
309236
let selectedIDs = Array(state.selectedIDs)
@@ -314,17 +241,14 @@ struct ArchiveAlbumDetailFeature {
314241
else { try await archiveClient.deletePhotoList(selectedIDs) }
315242
}))
316243
}
317-
318244
case .deletePhotosResponse(.success):
319245
let idsToDelete = state.selectedIDs
320246
state.photos.removeAll { idsToDelete.contains($0.id) }
321247
state.isSelectionMode = false
322248
state.selectedIDs.removeAll()
323249
return .send(.delegate(.showToast(NekiToastItem("사진을 삭제했어요", style: .success))))
324-
325250
case .deletePhotosResponse(.failure):
326251
return .send(.delegate(.showToast(NekiToastItem("사진을 삭제하지 못했어요", style: .error))))
327-
328252
case let .onTapExecuteDeleteAlbum(option):
329253
let shouldDeletePhotos = (option == .withPhotos)
330254
let albumId = state.album.id
@@ -333,49 +257,36 @@ struct ArchiveAlbumDetailFeature {
333257
try await archiveClient.deleteFolders([albumId], shouldDeletePhotos)
334258
}))
335259
}
336-
337260
case .deleteAlbumResponse(.success):
338261
return .run { send in
339262
await send(.delegate(.showToast(NekiToastItem("앨범을 삭제했어요", style: .success))))
340263
await dismiss()
341264
}
342-
343265
case .deleteAlbumResponse(.failure):
344266
return .send(.delegate(.showToast(NekiToastItem("앨범을 삭제하지 못했어요", style: .error))))
345267

346268
case .onTapImportPhotos:
347269
state.showDropDownMenu = false
348-
state.photoImport = PhotoImportFeature.State()
270+
state.photoImport = PhotoImportFeature.State(targetAlbumId: state.album.id) // 목적지 ID 주입
349271
return .none
350272

351273
case let .photoImport(.presented(.delegate(delegateAction))):
352274
switch delegateAction {
353-
case .didImportPhotos:
275+
case let .didCompleteTask(message):
354276
state.photoImport = nil
355-
state.isLoading = true
356-
357-
return .run { send in
358-
// TODO: 선택한 사진들을 현재 앨범으로 복제하는 API 호출
359-
try? await Task.sleep(for: .seconds(1)) // 임시 딜레이
360-
await send(.importPhotosResponse(.success(())))
361-
}
277+
return .merge(
278+
.send(.delegate(.showToast(NekiToastItem(message, style: .success)))),
279+
.send(.fetchPhotos)
280+
)
362281

363282
case .didTapCancel:
364283
state.photoImport = nil
365284
return .none
285+
286+
case let .showToast(toastItem):
287+
return .send(.delegate(.showToast(toastItem)))
366288
}
367289

368-
case .importPhotosResponse(.success):
369-
state.isLoading = false
370-
return .merge(
371-
.send(.delegate(.showToast(NekiToastItem("사진을 앨범에 가져왔어요", style: .success)))),
372-
.send(.fetchPhotos)
373-
)
374-
375-
case .importPhotosResponse(.failure):
376-
state.isLoading = false
377-
return .send(.delegate(.showToast(NekiToastItem("사진을 가져오지 못했어요", style: .error))))
378-
379290
default: return .none
380291
}
381292
}

0 commit comments

Comments
 (0)