Skip to content

Commit 1e2c72b

Browse files
authored
[Merge] #192 - 3차 스프린트 이미지 관련 변경점 반영
2 parents dd31170 + a2562b9 commit 1e2c72b

32 files changed

Lines changed: 929 additions & 465 deletions

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/Data/DTO/PresignedURLRequestDTO.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ public struct PresignedURLRequestData: Encodable {
1515
public let filename: String
1616
public let contentType: String
1717
public let mediaType: String
18+
public let width, height, size: Int?
1819
}

Neki-iOS/Core/Sources/ImagePicker/Data/DefaultImageUploadRepository.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ public struct DefaultImageUploadRepository: ImageUploadRepository {
2424
PresignedURLRequestData(
2525
filename: UUID().uuidString,
2626
contentType: item.contentType,
27-
mediaType: mediaType.rawValue
27+
mediaType: mediaType.rawValue,
28+
width: item.width,
29+
height: item.height,
30+
size: item.size
2831
)
2932
}
3033
let requestDTO = PresignedURLRequestDTO(items: requestItems)

Neki-iOS/Core/Sources/ImagePicker/Domain/ImageUploadClient.swift

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
// Created by OneTen on 1/21/26.
66
//
77

8+
import Foundation
89
import ComposableArchitecture
9-
import SwiftUI
10-
import PhotosUI
1110

1211
public struct ImageUploadClient {
1312
public var upload: @Sendable (_ data: [ImageUploadEntity], _ mediaType: ImageMediaType) async throws -> [Int]
1413
public var uploadConcurrentlyFromURLs: @Sendable (_ fileURLs: [URL], _ mediaType: ImageMediaType) async throws -> [Int]
15-
public var convert: @Sendable (_ items: [PhotosPickerItem]) async -> [ImageUploadEntity]
1614
}
1715

1816
extension ImageUploadClient: DependencyKey {
@@ -33,31 +31,23 @@ extension ImageUploadClient: DependencyKey {
3331
var entities: [ImageUploadEntity] = []
3432
for url in chunk {
3533
let data = try Data(contentsOf: url)
36-
entities.append(ImageUploadEntity(data: data, format: data.detectedImageFormat))
34+
let dimensions = data.imageDimensions
35+
36+
entities.append(
37+
ImageUploadEntity(
38+
data: data,
39+
format: data.detectedImageFormat,
40+
width: dimensions?.width,
41+
height: dimensions?.height,
42+
size: data.count
43+
)
44+
)
3745
}
3846

3947
let resultIDs = try await repository.upload(items: entities, mediaType: mediaType)
4048
uploadedMediaIDs.append(contentsOf: resultIDs)
4149
}
4250
return uploadedMediaIDs
43-
},
44-
45-
convert: { items in
46-
await withTaskGroup(of: ImageUploadEntity?.self) { group in
47-
for item in items {
48-
group.addTask {
49-
guard let data = try? await item.loadTransferable(type: Data.self) else { return nil }
50-
return ImageUploadEntity(data: data, format: data.detectedImageFormat)
51-
}
52-
}
53-
54-
var results: [ImageUploadEntity] = []
55-
for await result in group {
56-
guard let result else { continue }
57-
results.append(result)
58-
}
59-
return results
60-
}
6151
}
6252
)
6353
}()

Neki-iOS/Core/Sources/ImagePicker/Domain/ImageUploadEntity.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,27 @@ public struct ImageUploadEntity: Identifiable, Sendable {
1212
public let data: Data
1313
public let format: ImageFileFormat
1414

15+
public let width: Int?
16+
public let height: Int?
17+
public let size: Int?
18+
1519
public var contentType: String { format.contentType }
1620
public var fileExtension: String { format.fileExtension }
1721

1822
public init(
1923
id: UUID = UUID(),
2024
data: Data,
21-
format: ImageFileFormat
25+
format: ImageFileFormat,
26+
width: Int? = nil,
27+
height: Int? = nil,
28+
size: Int? = nil
2229
) {
2330
self.id = id
2431
self.data = data
2532
self.format = format
33+
self.width = width
34+
self.height = height
35+
self.size = size
2636
}
2737
}
2838

Neki-iOS/Core/Sources/ImagePicker/Presentation/Extension/Data+.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,22 @@ extension Data {
4242
else { return nil }
4343
return UTType(typeIdentifier as String)
4444
}
45+
46+
var imageDimensions: (width: Int, height: Int)? {
47+
let options: [CFString: Any] = [kCGImageSourceShouldCache: false]
48+
49+
guard let source = CGImageSourceCreateWithData(self as CFData, options as CFDictionary),
50+
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, options as CFDictionary) as? [CFString: Any] else {
51+
return nil
52+
}
53+
54+
var width = properties[kCGImagePropertyPixelWidth] as? Int
55+
var height = properties[kCGImagePropertyPixelHeight] as? Int
56+
57+
if let w = width, let h = height {
58+
return (w, h)
59+
}
60+
61+
return nil
62+
}
4563
}

0 commit comments

Comments
 (0)