Conversation
ImageKey를 dev~.png형태로 자르는 truncatedImageKeyString() 추가
비동기 처리가 2초 미만으로 걸릴 때를 대비하기 위함
png가 무거워서 생각보다 많이 느려지는 현상 발생..ㅜ
| @@ -18,11 +18,11 @@ struct SolplyPhotosPicker: View { | |||
| @State private var selectedImages: [UIImage] = [] | |||
|
|
|||
| private let maxSelectionCount: Int = 3 | |||
There was a problem hiding this comment.
최대 사진 첨부 개수가 3개라 상수로 선언하고 사용합니다!
| /// 문자열을 지정한 문자열 이전까지 잘라낸 새로운 문자열을 반환합니다. | ||
| /// - Parameter excludeEndRange: 이 문자열 직전까지만 남기고 잘라낼 기준 문자열. | ||
| /// - Returns: 기준 문자열 직전까지의 부분 문자열. | ||
| func truncated(excludeEndRange: String) -> String { |
There was a problem hiding this comment.
반환 타입을 Self 대신 String으로 명시한 거 좋네유
Self는 프로토콜 확장에서 타입을 제너릭하게 유지하려고 쓰는 건디... String 확장이니까 글케 바꾼거조
| import Foundation | ||
|
|
||
| struct ReportsState { | ||
| var placeId: Int = -1 |
There was a problem hiding this comment.
placeId: Int?로 두고 유효성은 guard let/if let같은 옵셔널로 처리하는 건 어떠신지...ㅎㅎ(셈나 과제라 아는 척 해볾)
현재도 Store에서 > 0 가드가 있어 의도는 잘 보입니다만, 의미를 타입으로 표현해브앙
There was a problem hiding this comment.
ReportsView에서 onAppear과 동시에 초기 placeId를 state에 저장하기 때문에 nil일 상황이 없어서 Optional로 처리하지 않았는데요, 그래도 혹시 모를 상황이 생길 수 있으니 Optional로 처리하는 게 의미상 더 맞는 거 같네요. 수정할게요!
| for (info, data) in zip(presignedInformation, imageDatas) { | ||
| if let url = URL(string: info.presignedUrl) { | ||
| presignedDictionary[url] = data.1 | ||
| } | ||
| } |
There was a problem hiding this comment.
zip으로 presigned 응답과 로컬 이미지 데이터를 1:1 매칭해서 딕셔너리 만들거군뇨
| self.dispatch(result) | ||
| } | ||
|
|
||
| case .presignedUrlReqeustSubmitted(let response): |
There was a problem hiding this comment.
Reqeust → Request 오타 발견 ㅋ
| if let selectedReportsType = state.selectedReportsType, reportsStep == .reportsComplete { | ||
| if state.attachedImageData.isEmpty { | ||
| dispatch( | ||
| .submitReports( | ||
| placeId: state.placeId, | ||
| request: ReportsRequestDTO( | ||
| reportType: selectedReportsType.rawValue, | ||
| content: state.reportsContent, | ||
| imageKeys: nil | ||
| ) | ||
| ) | ||
| ) | ||
| } else { | ||
| dispatch( | ||
| .submitPresignedUrlRequest( | ||
| request: PresignedUrlRequestDTO( | ||
| files: state.attachedImageData.map { fileName, _ in | ||
| File(fileName: fileName) | ||
| } | ||
| ) | ||
| ) | ||
| ) | ||
| } |
There was a problem hiding this comment.
이미지 없으면 바로 제보, 있으면 presigned → 업로드 → 제보 맞저
| func uploadImages(_ dictionary: [URL: Data]) async throws -> [URL] { | ||
| var uploadedUrls: [URL] = [] | ||
| print("📷 [UploadPhotosService] S3 사진 업로드 시작") | ||
| try await withThrowingTaskGroup(of: URL.self) { group in |
There was a problem hiding this comment.
이건 좋네요—withThrowingTaskGroup으로 병렬 업로드 가넝학겟슨
콩코롱시... 이거 배우고 갑니다 ㅋ
pedro0527
left a comment
There was a problem hiding this comment.
아빠 너무 어려워요 ㅜㅡㅜ 회사 점심시간 이제 끝남....나머지 이따 볼게...ㅎ
| func convertToJPGFile(compressionQuality: CGFloat = 0.8) -> (String, Data)? { | ||
| guard let data = self.jpegData(compressionQuality: compressionQuality) else { return nil } | ||
| let uuid = UUID().uuidString.prefix(8) | ||
| let fileName = "place\(uuid).jpg" | ||
| return (fileName, data) | ||
| } | ||
| } |
There was a problem hiding this comment.
임의의 이미지 이름을 정하기 위한 값입니다! 8 글자면 충분히 이미지들끼리 이름이 겹치지 않을 거 같아서 정했어염
| func uploadImages(_ dictionary: [URL: Data]) async throws -> [URL] { | ||
| var uploadedUrls: [URL] = [] | ||
| print("📷 [UploadPhotosService] S3 사진 업로드 시작") | ||
| try await withThrowingTaskGroup(of: URL.self) { group in | ||
| for (presignedUrl, data) in dictionary { | ||
| group.addTask { | ||
| try await self.uploadToS3(url: presignedUrl, data: data) | ||
| return presignedUrl | ||
| } | ||
| } | ||
|
|
||
| for try await url in group { | ||
| uploadedUrls.append(url) | ||
| } | ||
| } | ||
| print("📸 [UploadPhotosService] S3 사진 업로드 완료") | ||
| return uploadedUrls | ||
| } |
There was a problem hiding this comment.
PR을 보면 리턴한는거 아니고 각 비동기 작업의 리턴값이라고 하고
그 밑에서 리턴값을 받아서 최종적으로 리턴하는거 같은데
왜 그래여...? 모르게써
There was a problem hiding this comment.
일단 저 group에 묶여있는 Task들은 비동기적으로 병렬처리되고, 그 저리된 작업을 아래 for try await 구문에서 받아 배열에 append 해요 그리고 업로드 성공한 url을 반환하는 형식입니다.
우선 S3에 업로드는 한번에 한 사진만 가능합니다. 그렇기 때문에 사진 하나 끝날 때까지 기다렸다가 다음 사진 업로드 하고.. 이러면 너무 오래걸리기 때문에 병렬로 처리하는 거고, 그 사진마다 용량이 다를 것이기 때문에 항상 동시에 끝남을 보장받을 수 없어요. 그래서 비동기적으로 업로드합니다. 마지막으로 이 3장의 사진이 모두 업로드됐다고 하면(비동기 작업의 리턴값들이 모두 모이면) [URL]을 반환하는 거죠
There was a problem hiding this comment.
S3에 업로드하는 과정은 URLSession 사용해서 여기서는 안쓰는거죠ㅕ
| for (info, data) in zip(presignedInformation, imageDatas) { | ||
| if let url = URL(string: info.presignedUrl) { | ||
| presignedDictionary[url] = data.1 | ||
| } | ||
| } |

📄 작업 내용
💻 주요 코드 설명
truncated 함수 오버로딩
기존에는 문자열의 길이 또는 특정 문자열을 받아서 자르는 함수가 따로 존재했는데
이번에 하나 더 필요해서 매개변수만 다르게 받아 처리하도록 수정했어요
S3 업로드
일단 S3 업로드 과정은 다음과 같은데요
1. 클라 -> 서버 : 업로드 요청
2. 클라 -> S3 : 직접 업로드
3. 클라 -> 서버 : 업로드 완료 알림
위 과정을
Store&Effect에서 처리했어요제보하기 요청과 동시에 로띠를 돌리고 돌리는 중에 이미지를 업로드 & 제보하기 API 호출하도록 했습니다
(PresignedUrl 요청 -> S3 업로드 -> 제보하기 API 호출과정을 순차적으로 처리)
사진 업로드가 오래 걸릴수도 있기 때문에 제보하기 API가 성공했을 때 로띠를 멈추도록 했고, 최소 2초는 로띠가 돌아가도록 했어요
1. 클라 -> 서버 : 업로드 요청
사진 선택은 필수가 아니기 때문에 선택된 사진이 없다면
DTO에imageKeys에nil을 담고제보하기 API를 호출하는
submitReportsaction을 dispatch합니다.선택된 사진이 하나라도 있으면 S3 업로드 과정으로 돌입합니다.
그러니
presigned Url요청을 먼저 해야겠죠?(
submitPresignedUrlRequestdispatch)서버에게 이미지를 S3에 올릴
presigned URL을 요청합니다.서버로부터 성공적으로
presigned URL를 받으면 사진을 직접 업로드를 해야합니다.2. 클라 -> S3 : 직접 업로드
presigned URL를 통해 사진을 S3에 업로드 해요effect에서는uploadPhotosService를 통해 업로드를 하는데요업로드할 사진과 URL을
[URL: Data]형태의 딕셔너리로 가공해서effect로 넘겨요effect에서는 이 딕셔너리를 통해service의 API 요청 함수를 호출하겠죠effect에서는 위
UploadPhotosService의 funcuploadImages()함수를 호출하여 업로드를 진행합니다.사진 여러 개를 순차적으로 처리하면 오래 걸리기 때문에
withThrowingTaskGroup를 통해 병렬처리했어요group.addTask { ... }코드는 백그라운드에서 동시에 처리합니다.S3로 업로드하는 과정은
URLSession을 사용했어요3. 클라 -> 서버 : 업로드 완료 알림
S3 업로드 후 업로드 한 key를 제보하기 API에 같이 담아서 보내줘요
아까 사진을 선택하지 않았을 때
imageKeys를 nil로 담아서 보내줬죠?이제는 S3에서 응답으로 받은 imagekey들을
[String]형태로 전환하여DTO에 잘 담아서 보내주면 됩니다.이렇게
DTO로 잘 변환 후 제보하기 API를 호출합니다.제보하기에 성공하면 로띠 재생을 끝내요
로띠 최소 2초 재생
제보하기 버튼을 누르면 로띠를 재생함과 동시에 PresignedUrl 요청 & S3 업로드 & 제보하기 API 호출을 순차적으로 진행하기 때문에
이 과정이 다 끝나면 로띠 재생을 멈추도록 구현했어요
그래서 우리 최소 로띠 재생시간인 2초보다 덜 걸릴수도, 더 걸릴수도 있기 때문에 최소 2초를 보장하고자
아래와 같이 제보하기 API가 끝나도 최소 2초는 기다리도록 구현했어요
=> 그래서 gif보면, 사진 첨부하면 2초보다 조금 더 걸려서 로띠 사이클이 1회보다 조금 더 돌아요, 사진 없으면 로띠 1사이클 돌고 바로 끝남
(아직 제보하기 에러처리에 대한 뷰가 없어서 제보를 성공했을 때만 최소 2초 돌아가도록 했습니다.)
🔗 연결된 이슈
👀 기타 더 이야기해볼 점
error로 반환) 이 부분에 대한 피그마가 없어서 처리하지 않았습니다(제보 실패하면 로띠가 일찍 끝남. 이때Toast띄우거나 하면 될듯)