Skip to content

Commit bc9e402

Browse files
committed
[Refactor] #192 - S3 저장소 낭비 방지를 위한 업로드 플로우 통합
1 parent b741cab commit bc9e402

7 files changed

Lines changed: 151 additions & 349 deletions

File tree

Neki-iOS/APP/Sources/MainTab/MainTabCoordinator.swift

Lines changed: 20 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ struct MainTabCoordinator {
2424
var archive = ArchiveCoordinator.State()
2525
var map = MapCoordinator.State()
2626
var myPage: MyPageCoordinator.State
27-
var imagePicker = ImagePickerFeature.State(mediaType: .photoBooth)
27+
28+
var imagePicker = ImagePickerFeature.State(mediaType: .photoBooth, autoUpload: false)
29+
2830
var isPhotoPickerPresented: Bool = false
2931
var pendingPresentation: PendingPresentation?
3032
var isLoading: Bool = false
31-
3233

3334
@Presents var destination: Destination.State?
3435

@@ -81,37 +82,19 @@ struct MainTabCoordinator {
8182
var body: some ReducerOf<Self> {
8283
BindingReducer()
8384

84-
Scope(state: \.archive, action: \.archive) {
85-
ArchiveCoordinator()
86-
}
87-
88-
Scope(state: \.pose, action: \.pose) {
89-
PoseCoordinator()
90-
}
91-
92-
Scope(state: \.map, action: \.map) {
93-
MapCoordinator()
94-
}
95-
96-
Scope(state: \.myPage, action: \.myPage) {
97-
MyPageCoordinator()
98-
}
99-
100-
Scope(state: \.imagePicker, action: \.imagePicker) {
101-
ImagePickerFeature()
102-
}
85+
Scope(state: \.archive, action: \.archive) { ArchiveCoordinator() }
86+
Scope(state: \.pose, action: \.pose) { PoseCoordinator() }
87+
Scope(state: \.map, action: \.map) { MapCoordinator() }
88+
Scope(state: \.myPage, action: \.myPage) { MyPageCoordinator() }
89+
Scope(state: \.imagePicker, action: \.imagePicker) { ImagePickerFeature() }
10390

10491
Reduce { (state: inout State, action: Action) -> Effect<Action> in
10592
switch action {
106-
case .binding:
107-
return .none
108-
93+
case .binding: return .none
10994
case .onTapAddButton:
11095
state.destination = .uploadSelection
11196
return .none
11297

113-
114-
// MARK: - Gallary Logic
11598
case .onTapGallery:
11699
guard state.destination != nil else { return .none }
117100
state.pendingPresentation = .gallery
@@ -121,23 +104,18 @@ struct MainTabCoordinator {
121104
case .uploadSelectionSheetDismissed:
122105
guard let pendingPresentation = state.pendingPresentation else { return .none }
123106
state.pendingPresentation = nil
124-
125107
switch pendingPresentation {
126-
case .gallery:
127-
return .send(.setPhotosPickerPresented(true))
108+
case .gallery: return .send(.setPhotosPickerPresented(true))
128109
}
129110

130111
case let .setPhotosPickerPresented(isPresented):
131112
state.isPhotoPickerPresented = isPresented
132113
return .none
133114

134-
// MARK: - QR Scan Logic
135115
case .onTapQRScan:
136116
state.destination = nil
137117
switch qrScannerClient.checkAuthorizationStatus() {
138-
case .authorized:
139-
return .send(.qrScannerPresented)
140-
118+
case .authorized: return .send(.qrScannerPresented)
141119
case .notDetermined:
142120
return .run { send in
143121
let isAuthorized = await qrScannerClient.requestAccess()
@@ -171,18 +149,14 @@ struct MainTabCoordinator {
171149
await openURL(url)
172150
}
173151

174-
175-
// MARK: - Child Features Logic
176-
case .archive(.delegate(.requestQRScan)):
152+
case .archive(.delegate(.requestQRScan)), .pose(.delegate(.requestQRScan)):
177153
state.destination = nil
178154
return .send(.onTapQRScan)
179155

180156
case let .archive(.delegate(.showToast(item))):
181157
state.toast = item
182158
return .none
183159

184-
case .pose(.delegate(.requestQRScan)):
185-
return .send(.onTapQRScan)
186160

187161
case .myPage(.delegate(.didLogout)):
188162
return .run { send in
@@ -199,22 +173,12 @@ struct MainTabCoordinator {
199173
case let .myPage(.delegate(.profileUpdated(user))):
200174
return .send(.delegate(.profileUpdated(user)))
201175

202-
case .imagePicker(.uploadStarted):
203-
state.isLoading = true
204-
return .none
205-
206-
case let .imagePicker(.uploadCompleted(imageIDs)):
176+
case let .imagePicker(.delegate(.imagesConverted(entities))):
207177
state.isPhotoPickerPresented = false
208178
state.isLoading = false
209179
state.selectedTab = .archive
210-
guard imageIDs.isEmpty == false else { return .none }
211-
return .send(.archive(.root(.processUploadImages(imageIDs: imageIDs))))
212-
213-
case .imagePicker(.uploadFailed):
214-
state.isPhotoPickerPresented = false
215-
state.isLoading = false
216-
state.toast = NekiToastItem("이미지 업로드에 실패했어요.", style: .error)
217-
return .none
180+
guard !entities.isEmpty else { return .none }
181+
return .send(.archive(.root(.processUploadImages(entities: entities))))
218182

219183
case let .destination(.presented(.qrScan(.addPhotoFromQRScanner(imageID)))):
220184
state.destination = nil
@@ -234,48 +198,25 @@ struct MainTabCoordinator {
234198
}
235199
.ifLet(\.$destination, action: \.destination)
236200

237-
/// 피그마 확인 결과 탭바가 사라지는 모든 case는 depth가 1 이상일 경우더라구요
238-
/// 즉, 메인 홈 화면에서 depth가 추가되어 넘어가는 뷰들은 전부 탭바가 사라집니다.
239-
/// 그래서 각 Feature의 state에서 path에 하나라도 추가될 경우 탭바를 가리게 설계했습니다.
240-
/// 한 가지 문제는, 나중에 depth가 추가되어도 탭바가 보여져야 하는 경우가 생긴다면 다시 머리 싸매야함
241201
Reduce { (state: inout State, action: Action) -> Effect<Action> in
242202
switch state.selectedTab {
243-
case .archive:
244-
state.isTabbarHidden = !state.archive.path.isEmpty
245-
246-
case .pose:
247-
state.isTabbarHidden = !state.pose.path.isEmpty
248-
249-
case .add:
250-
return .none
251-
252-
case .map:
253-
state.isTabbarHidden = !state.map.path.isEmpty
254-
255-
case .myPage:
256-
state.isTabbarHidden = !state.myPage.path.isEmpty
203+
case .archive: state.isTabbarHidden = !state.archive.path.isEmpty
204+
case .pose: state.isTabbarHidden = !state.pose.path.isEmpty
205+
case .add: return .none
206+
case .map: state.isTabbarHidden = !state.map.path.isEmpty
207+
case .myPage: state.isTabbarHidden = !state.myPage.path.isEmpty
257208
}
258-
259209
return .none
260210
}
261211
}
262212
}
263213

264-
265-
// MARK: - Child Reducer
266-
267214
extension MainTabCoordinator {
268215
@Reducer
269216
enum Destination {
270217
case uploadSelection
271218
case qrScan(QRCodeScanFeature)
272219
}
273-
}
274-
275-
276-
// MARK: - Nested Types
277-
278-
extension MainTabCoordinator {
279220
enum PendingPresentation {
280221
case gallery
281222
}

Neki-iOS/Core/Sources/ImagePicker/Presentation/ImagePickerFeature.swift

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,36 @@ public struct ImagePickerFeature {
1919
public var maxCount: Int
2020
public let mediaType: ImageMediaType
2121

22+
public var autoUpload: Bool
23+
2224
public var selectedImages: IdentifiedArrayOf<ImageUploadEntity> = []
2325
public var pickerItems: [PhotosPickerItem] = []
2426
public var isLoading: Bool = false
2527

2628
public var remainingCount: Int { max(.zero, maxCount - selectedImages.count) }
2729

28-
public init(maxCount: Int = 10, mediaType: ImageMediaType) {
30+
public init(maxCount: Int = 10, mediaType: ImageMediaType, autoUpload: Bool = true) {
2931
self.maxCount = maxCount
3032
self.mediaType = mediaType
33+
self.autoUpload = autoUpload
3134
}
3235
}
3336

3437
public enum Action: BindableAction {
3538
case binding(BindingAction<State>)
3639

37-
// User Action
3840
case pickerItemsChanged([PhotosPickerItem])
39-
40-
// Internal Action (Data Load Complete)
4141
case imageConverted([ImageUploadEntity])
4242

43-
// Upload Action
4443
case requestUpload
4544
case uploadStarted
4645
case uploadCompleted([Int])
4746
case uploadFailed
47+
48+
case delegate(Delegate)
49+
public enum Delegate {
50+
case imagesConverted([ImageUploadEntity])
51+
}
4852
}
4953

5054
@Dependency(\.imageUploadClient) var imageUploadClient
@@ -70,7 +74,15 @@ public struct ImagePickerFeature {
7074
if remaining > 0 { state.selectedImages.append(contentsOf: newEntities.prefix(remaining)) }
7175

7276
state.pickerItems.removeAll()
73-
return .send(.requestUpload)
77+
78+
if state.autoUpload {
79+
return .send(.requestUpload)
80+
} else {
81+
state.isLoading = false
82+
let validEntities = Array(state.selectedImages)
83+
state.selectedImages.removeAll()
84+
return .send(.delegate(.imagesConverted(validEntities)))
85+
}
7486

7587
case .requestUpload:
7688
guard !state.selectedImages.isEmpty else {
@@ -102,17 +114,14 @@ public struct ImagePickerFeature {
102114

103115
default:
104116
return .none
105-
106117
}
107118
}
108119
}
109-
}
110-
111-
112-
// MARK: - ImagePickerFeature Private Func
113-
114-
private extension ImagePickerFeature {
115-
static func convert(items: [PhotosPickerItem]) async -> [ImageUploadEntity] {
120+
121+
122+
// MARK: - Private Methods
123+
124+
private static func convert(items: [PhotosPickerItem]) async -> [ImageUploadEntity] {
116125
await withTaskGroup(of: ImageUploadEntity?.self) { group in
117126
for item in items {
118127
group.addTask {
@@ -129,7 +138,6 @@ private extension ImagePickerFeature {
129138
)
130139
}
131140
}
132-
133141
var results: [ImageUploadEntity] = []
134142
for await result in group {
135143
guard let result else { continue }
@@ -139,10 +147,8 @@ private extension ImagePickerFeature {
139147
}
140148
}
141149

142-
static func detectFormat(from data: Data) -> ImageFileFormat {
143-
// 데이터가 너무 짧으면 기본값(JPEG) 반환
150+
private static func detectFormat(from data: Data) -> ImageFileFormat {
144151
guard data.count > 12 else { return .jpeg }
145-
146152
let header = data.prefix(12)
147153
let firstByte = header[0]
148154

@@ -159,22 +165,20 @@ private extension ImagePickerFeature {
159165
header[8] == 0x57 && header[9] == 0x45 && header[10] == 0x42 && header[11] == 0x50 { // "WEBP"
160166
return .webp
161167
}
162-
163-
// 나머지는 JPEG로
164168
return .jpeg
165169
}
166170

167-
static func extractDimensions(from data: Data) -> (width: Int, height: Int)? {
171+
private static func extractDimensions(from data: Data) -> (width: Int, height: Int)? {
168172
let options: [CFString: Any] = [kCGImageSourceShouldCache: false]
169-
170173
guard let source = CGImageSourceCreateWithData(data as CFData, options as CFDictionary),
171174
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, options as CFDictionary) as? [CFString: Any] else {
172175
return nil
173176
}
174-
175177
var width = properties[kCGImagePropertyPixelWidth] as? Int
176178
var height = properties[kCGImagePropertyPixelHeight] as? Int
177-
179+
if let orientation = properties[kCGImagePropertyOrientation] as? Int, (5...8).contains(orientation) {
180+
swap(&width, &height)
181+
}
178182
if let w = width, let h = height { return (w, h) }
179183
return nil
180184
}

0 commit comments

Comments
 (0)