Skip to content

Commit 8eb4498

Browse files
authored
[Merge] #206 - 이미지 앨범 이동 및 복제 API 연결
2 parents bdcf41d + ecc6890 commit 8eb4498

8 files changed

Lines changed: 128 additions & 14 deletions

File tree

Neki-iOS/Features/Archive/Sources/Data/Sources/ArchiveEndpoint.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public enum ArchiveEndpoint {
2020
case excludePhotosInAlbum(albumID: Int, request: DeletePhotoRequestDTO)
2121
case editFolderName(albumID: Int, request: FolderDTO.Request)
2222
case updateMemo(photoID: Int, request: UpdateMemoRequestDTO)
23+
case duplicatePhoto(request: UpdateMappingPhotoDTO.DuplicatePhotos)
24+
case movePhoto(request: UpdateMappingPhotoDTO.MovePhotos)
2325
}
2426

2527
extension ArchiveEndpoint: Endpoint {
@@ -61,6 +63,10 @@ extension ArchiveEndpoint: Endpoint {
6163
return "folders/\(albumID)"
6264
case .updateMemo(let id, _):
6365
return "photos/\(id)"
66+
case .duplicatePhoto:
67+
return "folders/photos/copy"
68+
case .movePhoto:
69+
return "folders/photos/move"
6470
}
6571
}
6672

@@ -122,6 +128,10 @@ extension ArchiveEndpoint: Endpoint {
122128
return .patch
123129
case .updateMemo:
124130
return .put
131+
case .duplicatePhoto:
132+
return .post
133+
case .movePhoto:
134+
return .patch
125135
}
126136
}
127137

@@ -151,6 +161,10 @@ extension ArchiveEndpoint: Endpoint {
151161
return request
152162
case .updateMemo(_, let request):
153163
return request
164+
case .duplicatePhoto(let request):
165+
return request
166+
case .movePhoto(let request):
167+
return request
154168
}
155169
}
156170
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// UpdateMappingPhotoDTO.swift
3+
// Neki-iOS
4+
//
5+
// Created by OneTen on 4/12/26.
6+
//
7+
8+
import Foundation
9+
10+
public enum UpdateMappingPhotoDTO {
11+
public struct MovePhotos: Encodable {
12+
let sourceFolderID: Int
13+
let photoIDs, targetFolderIDs: [Int]
14+
15+
enum CodingKeys: String, CodingKey {
16+
case sourceFolderID = "sourceFolderId"
17+
case photoIDs = "photoIds"
18+
case targetFolderIDs = "targetFolderIds"
19+
}
20+
}
21+
22+
public struct DuplicatePhotos: Encodable {
23+
let photoIDs, targetFolderIDs: [Int]
24+
25+
enum CodingKeys: String, CodingKey {
26+
case photoIDs = "photoIds"
27+
case targetFolderIDs = "targetFolderIds"
28+
}
29+
}
30+
}

Neki-iOS/Features/Archive/Sources/Data/Sources/DefaultArchiveRepository.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,44 @@ extension DefaultArchiveRepository {
306306
}
307307

308308
}
309+
310+
func duplicatePhoto(photoIDs: [Int], targetFolderIDs: [Int]) async throws {
311+
let request = UpdateMappingPhotoDTO.DuplicatePhotos(
312+
photoIDs: photoIDs,
313+
targetFolderIDs: targetFolderIDs
314+
)
315+
let endpoint = ArchiveEndpoint.duplicatePhoto(request: request)
316+
let _ = try await networkProvider.request(endpoint: endpoint)
317+
318+
// 앨범 정보 캐시 갱신
319+
self.isAlbumCacheDirty = true
320+
321+
// Target 폴더들의 캐시를 다음 진입 시 새로 받아오도록 유도
322+
for targetID in targetFolderIDs {
323+
self.isPhotoCacheDirty[targetID] = true
324+
}
325+
}
326+
327+
func movePhoto(sourceFolderId: Int, photoIDs: [Int], targetFolderIDs: [Int]) async throws {
328+
let request = UpdateMappingPhotoDTO.MovePhotos(
329+
sourceFolderID: sourceFolderId,
330+
photoIDs: photoIDs,
331+
targetFolderIDs: targetFolderIDs
332+
)
333+
let endpoint = ArchiveEndpoint.movePhoto(request: request)
334+
let _ = try await networkProvider.request(endpoint: endpoint)
335+
336+
// 앨범 정보 캐시 갱신
337+
self.isAlbumCacheDirty = true
338+
339+
self.isPhotoCacheDirty[sourceFolderId] = true
340+
341+
// Target 폴더(혹은 전체 사진) 캐시 갱신
342+
for targetID in targetFolderIDs {
343+
self.isPhotoCacheDirty[targetID] = true
344+
}
345+
}
346+
309347
}
310348

311349

Neki-iOS/Features/Archive/Sources/Domain/Sources/Client/ArchiveClient.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ struct ArchiveClient {
2424
public var editAlbumName: (_ albumID: Int, _ name: String) async throws -> Void
2525
public var updatePhotoMemo: (_ photoID: Int, _ memo: String) async throws -> Void
2626
public var clearCache: () async -> Void
27+
public var duplicatePhoto: (_ photoIDs: [Int], _ targetFolderIDs: [Int]) async throws -> Void
28+
public var movePhoto: (_ sourceFolderId: Int, _ photoIDs: [Int], _ targetFolderIDs: [Int]) async throws -> Void
2729
}
2830

2931
extension ArchiveClient: DependencyKey {
@@ -69,6 +71,12 @@ extension ArchiveClient: DependencyKey {
6971
},
7072
clearCache: {
7173
await archiveRepository.clearCache()
74+
},
75+
duplicatePhoto: { photoIDs, targetFolderIDs in
76+
try await archiveRepository.duplicatePhoto(photoIDs: photoIDs, targetFolderIDs: targetFolderIDs)
77+
},
78+
movePhoto: { sourceFolderId, photoIDs, targetFolderIDs in
79+
try await archiveRepository.movePhoto(sourceFolderId: sourceFolderId, photoIDs: photoIDs, targetFolderIDs: targetFolderIDs)
7280
}
7381
)
7482
}

Neki-iOS/Features/Archive/Sources/Domain/Sources/Interfaces/Repositories/ArchiveRepository.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ protocol ArchiveRepository: Sendable {
2323
func excludePhotosInAlbum(albumID: Int, photoIDs: [Int]) async throws
2424
func editAlbumName(albumID: Int, name: String) async throws
2525
func updatePhotoMemo(photoID: Int, memo: String) async throws
26+
func duplicatePhoto(photoIDs: [Int], targetFolderIDs: [Int]) async throws
27+
func movePhoto(sourceFolderId: Int, photoIDs: [Int], targetFolderIDs: [Int]) async throws
2628

2729
// Delete
2830
func deletePhotoList(photoIDs: [Int]) async throws

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,33 @@ struct AlbumSelectionFeature {
144144
case .tapConfirm:
145145
guard !state.selectedAlbumIDs.isEmpty else { return .none }
146146
state.isLoading = true
147+
147148
let purpose = state.selectionPurpose
149+
let photoIDs = state.photoIDs
150+
let targetFolderIDs = Array(state.selectedAlbumIDs)
151+
let currentAlbumId = state.currentAlbumId
148152

149153
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))
154+
do {
155+
switch purpose {
156+
case .duplicate:
157+
// 복제 - sourceFolderId 없음
158+
try await archiveClient.duplicatePhoto(photoIDs: photoIDs, targetFolderIDs: targetFolderIDs)
159+
await send(.taskCompleted(message: "사진을 앨범에 추가했어요"))
160+
161+
case .move:
162+
// 이동 - sourceFolderId 필수
163+
if let sourceFolderId = currentAlbumId {
164+
try await archiveClient.movePhoto(sourceFolderId: sourceFolderId, photoIDs: photoIDs, targetFolderIDs: targetFolderIDs)
165+
await send(.taskCompleted(message: "사진을 앨범으로 이동했어요"))
166+
} else {
167+
await send(.taskFailed(message: "사진 이동에 실패했어요"))
168+
}
169+
}
170+
} catch {
171+
let failMsg = purpose == .duplicate ? "사진 추가에 실패했어요" : "사진 이동에 실패했어요"
172+
await send(.taskFailed(message: failMsg))
173+
}
155174
}
156175

157176
case let .taskCompleted(message):

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ struct PhotoImportFeature {
2323
var isFetchingAlbums: Bool = false
2424
var isLoading: Bool = false
2525

26-
var targetAlbumId: Int
26+
var targetAlbumId: Int
2727

2828
var uploadCount: Int { selectedIDs.count }
2929
var isUploadEnabled: Bool { uploadCount > 0 }
@@ -121,7 +121,7 @@ struct PhotoImportFeature {
121121

122122
return .run { [id = state.selectedAlbum?.id] send in
123123
if id == -1 {
124-
await send(.fetchPhotosResponse(Result { try await archiveClient.fetchFavoritePhotoList(20, sortOrder) }))
124+
await send(.fetchPhotosResponse(Result { try await archiveClient.fetchFavoritePhotoList(size: 20, sortOrder: sortOrder) }))
125125
} else {
126126
await send(.fetchPhotosResponse(Result { try await archiveClient.fetchPhotoList(folderId: targetFolderId, size: 20, sortOrder: sortOrder) }))
127127
}
@@ -172,14 +172,17 @@ struct PhotoImportFeature {
172172
case .tapUpload:
173173
guard state.isUploadEnabled else { return .none }
174174
state.isLoading = true
175-
let ids = Array(state.selectedIDs)
176-
let targetId = state.targetAlbumId
175+
176+
let photoIDs = Array(state.selectedIDs)
177+
let targetFolderIDs = [state.targetAlbumId]
177178

178179
return .run { send in
179-
// TODO: 실제 API 연동 (archiveClient.duplicatePhoto(sourceFolderId: nil, photoIDs: ids, targetFolderIDs: [targetId]))
180-
try? await Task.sleep(for: .seconds(1)) // 임시 딜레이
181-
182-
await send(.taskCompleted(message: "사진을 앨범에 가져왔어요"))
180+
do {
181+
try await archiveClient.duplicatePhoto(photoIDs: photoIDs, targetFolderIDs: targetFolderIDs)
182+
await send(.taskCompleted(message: "사진을 앨범에 가져왔어요"))
183+
} catch {
184+
await send(.taskFailed(message: "사진을 가져오지 못했어요"))
185+
}
183186
}
184187

185188
case let .taskCompleted(message):

Neki-iOS/Features/Archive/Sources/Presentation/Sources/View/ArchiveAlbumDetailView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ private extension ArchiveAlbumDetailView {
223223
} label: {
224224
Text("앨범 삭제")
225225
.nekiFont(.body16Medium)
226-
.foregroundStyle(.primary500) // TODO: - 위험한 액션이니 빨간색 어떠냐고 피그마 문의 남김. 답변에 따라 수정가능성 있음
226+
.foregroundStyle(.primary500)
227227
}
228228
.frame(width: 120, height: 34, alignment: .leading)
229229
.padding(.leading, 12)

0 commit comments

Comments
 (0)