Skip to content

Conversation

@Guryss
Copy link
Member

@Guryss Guryss commented May 23, 2025

⭐️Issue


🌟Motivation

  • 피드작성뷰 내 이미지 첨부 기능 UI 구현했습니다.

해당 브랜치에서 구현된 기능 명세

  1. 피드글 작성 시 이미지의 업로드가 가능하다. ✅
  2. 피드글 수정 시 이미지의 업로드가 가능하다. ❌
  3. 이미지 아이콘 클릭시 최대 5장의 사진 업로드 가능하다. ✅
  4. 순서가 정해지는 이미지는 아직 구현하지 않는다. ✅
  5. 5장이 넘어가면 ‘5장까지 업로드 가능해요’ 라는 토스트를 띄운다. ✅
  6. 이미지 접근 권한은 이미지 아이콘 클릭시 요구한다. ⚠️
    -> 접근 권한을 필요로 하지 않는 PHPickerViewController를 사용하였기 때문에 해당 명세는 생략합니다.
  7. 접근 권한이 허용되지 않았다면 ‘이미지 접근 권한을 허용해 주세요!’ 라는 토스트를 띄운다. ⚠️
    -> 접근 권한을 필요로 하지 않는 PHPickerViewController를 사용하였기 때문에 해당 명세는 생략합니다.
  8. 작성(수정) 완료를 클릭하면 서버에 multipart 형태로 이미지를 전송한다. ❌ (수정시 반영이 안됨.)
  9. 피드 목록에서 대표 썸네일(첫 사진) 1장을 표시한다. ✅
  10. 피드 목록에서 대표 사진을 포함한 총 사진 수를 출력한다. ✅
  11. 피드 목록에서 해당 사진 클릭 시에도 피드 상세보기로 이동한다. ✅
  12. 피그마 상의 모서리가 둥근 정사각형 형태로 미리보기를 표시한다. ✅
  13. 피드 상세보기에서 이미지 미리보기를 클릭시 확대된다. ✅
  14. 확대된 이미지를 오른쪽으로 넘기면 다음 이미지로 이동한다. ✅
  15. 확대된 이미지 상단에 {현재페이지/남은페이지} 를 표시한다. ✅

구현안된 기능 명세

  1. 피드 수정 시 이미지 처리 로직
  2. 피드 작성 시 이미지 cell 내 X 버튼 클릭 시 삭제 로직

-> #571 이슈에서 진행합니다!

🌟Key Changes

🎆 이미지 파일 확장자에 따른 이미지 압축률 및 해상도 조절 함수 구현

  • HEIC 확장자 파일의 경우 -> 높은 압축률로 작은 크기를 갖지만 JPEG 확장자로 변환 시 크기가 약 3~4배로 커지게 됩니다.
    이미지 파일이 들어오고 해당 파일의 확장자가 HEIC 일 때 우선적으로 해상도 0.5로 설정되도록 합니다.

  • 이외의 확장자 파일일 경우
    기본 해상도 0.7로 설정하여 사이즈를 작게 만듭니다.
    0.25MB 보다 크다면 압축 품질을 0.1씩 낮추며 이미지 크기를 측정합니다.
    이후에도 조건을 만족하지 않는다면 해상도를 0.1씩 낮추면서 크기를 측정합니다.

각각의 이미지 파일마다 해당 함수를 거쳐 이미지 사이즈를 작게 만들기 때문에 피드 업로드 시 시간이 꽤나 걸리게 됩니다.
피드백할 부분이 있다면 부탁드립니다 ( _ _ )

func compressImages(_ images: [UIImage],
                                      maxImageSize: Int = MultipartConstants.maxImageSize) -> [Data] {
        // 압축된 이미지 파일 배열
        var compressedDatas: [Data] = []
        
        for (index, image) in images.enumerated() {
            
            // 이미지 압축 품질 설정
            var quality: CGFloat = 1.0
            
            // 이미지 기본 해상도 설정
            var scale: CGFloat = 0.7
            
            // HEIC 파일 타입 기본 해상도 설정 - 0.5
            if let cgImageSource = CGImageSourceCreateWithData(image.pngData()! as CFData, nil),
               let utiString = CGImageSourceGetType(cgImageSource) as String?,
               let utType = UTType(utiString),
               utType.conforms(to: .heic) {
                scale = 0.5
            }
            
            var data: Data? = image.resizedImage(to: scale)?.jpegData(compressionQuality: quality)

            while (data == nil || data!.count > maxImageSize) && quality > 0.01 && scale > 0.1 {
                if quality > 0.2 {
                    quality -= 0.1
                } else {
                    scale -= 0.1
                    if let resizedImage = image.resizedImage(to: scale) {
                        data = resizedImage.jpegData(compressionQuality: quality)
                    }
                    continue
                }
                data = image.resizedImage(to: scale)?.jpegData(compressionQuality: quality)
            }
            
            if let data = data, data.count <= maxImageSize {
                compressedDatas.append(data)
            } else {
                compressedDatas.append(Data())
            }
        }
        
        return compressedDatas
    }
}

🌟Simulation

  1. 피드 목록 서버 연결
1.mp4
  1. 피드 상세 서버 연결
1.mp4
  1. 피드 작성
ScreenRecording_05-25-2025.20-50-15_1.1.mp4

🌟To Reviewer

  • 커밋번호를 헷갈려서 실수로 556번으로 작성했네요 ㅜㅜ
    -> UI나 현재까지 구현된 기능들에서만 확인 부탁드립니다! 에러 발견이나 UX적으로 반영되면 좋겠는 부분이 있다면 언제든 환영!

🌟Reference


@Guryss Guryss self-assigned this May 23, 2025
@Guryss Guryss added this to WSS-iOS May 23, 2025
@Guryss Guryss moved this to In Progress in WSS-iOS May 23, 2025
@Guryss Guryss added this to the 사진소소 🤳 milestone May 23, 2025
@Guryss Guryss changed the title [Feat] #554 - 피드작성뷰 내 이미지 첨부 기능 UI 구현 [Feat] #554 - 피드작성뷰 내 이미지 첨부 기능 UI 구현 및 전반적인 서버 연결 May 25, 2025
@Guryss Guryss marked this pull request as ready for review May 25, 2025 12:19
Copy link
Member

@ena-isme ena-isme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다! 이미지 기능은 정말 신세계네요 ,,,

코드에 대한 코멘트만 남겼고,
시뮬로 확인 후 한 번 더 코리 하도록 하겠습니다 :)

Comment on lines +83 to +89
func compressImages(_ images: [UIImage],
maxImageSize: Int = MultipartConstants.maxImageSize) -> [Data] {
// 압축된 이미지 파일 배열
var compressedDatas: [Data] = []

for (index, image) in images.enumerated() {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W2
현재 해당 함수를 서버 단에서 호출하고 있는데, MainThread 에서 호출하고 있는 것으로 보입니다.

  1. 백그라운드 에서 처리할 수는 없을까요?
  2. 현재 for 문 안의 로직이 동기처리되고 있어 PR 작성해주신 대로 속도가 절감되고 있는 것 같습니다. 각각의 이미지를 병렬로 비동기 처리할 수는 없을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 그렇네요! .observe(on: MainScheduler.instance) 가 붙어있단 생각을 못했네요 하하
    이미지 압축 함수만 따로 빼서 GCD로 비동기 처리하여 백그라운드에서 처리할 수 있도록 하면 될 것 같아요!

  2. 해보겠습니닷! 찾아보니 DispatchGroup, async let 등등 구현 방법이 꽤 있는 것 같네요. 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 해당 테스크도 다음 #571 이슈로 넘겨서 작업할게요!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W3
withThrowingTaskGroup으로 작성하면 오류 처리를 일괄적으로 할 수 있어서 좋더라구요 !! 전에 프로젝트 할 때 사용했던 코드 첨부합니당!.!

    /// SwiftUI용으로 새로 나온 PhotosPicker 를 사용할 때 편하게 이미지를 업로드 할 수 있는 함수
    func postPhotosPickerItem(path: String, imageName: String, item: PhotosPickerItem) async throws -> String {
        guard let data = try? await item.loadTransferable(type: Data.self) else {
            throw FirebaseError.encodingFailed
        }
        
        let imagePath = "\(path)/\(imageName).jpg"
        return try await postData(path: imagePath, data: data)
    }
    
    /// 여러 PhotosPickerItem을 동시에 업로드 할때. post 코드는 병렬로 호출되어서 동시에 진행된다.
    func postPhotosPickerItems(path: String, imageNames: [String], items: [PhotosPickerItem]) async throws -> [String] {
        var results = Array(repeating: "", count: items.count)
        
        try await withThrowingTaskGroup(of: (Int, String).self) { group in
            for (index, item) in items.enumerated() {
                group.addTask {
                    let url = try await postPhotosPickerItem(path: path, imageName: imageNames[index], item: item)
                    return (index, url)
                }
            }
            
            for try await (index, url) in group {
                results[index] = url
            }
        }
        
        return results
    }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다. 다음 이슈에 써놓고 활용해볼게요!!! 👍🤩

Copy link
Contributor

@Naknakk Naknakk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

깔꼼하네요 고생했습니다!!! 이나가 제안해준 대로 비동기&동시 작업 처리 잘 되면 좋겠습니당~!~! 다만 백그라운드에서 돌리면 피드 리스트로 돌아갈 때나 업로드 실패 시 어떻게 처리해야할지가 조금 어려운 문제 같기도 하네용 . .

Comment on lines +83 to +89
func compressImages(_ images: [UIImage],
maxImageSize: Int = MultipartConstants.maxImageSize) -> [Data] {
// 압축된 이미지 파일 배열
var compressedDatas: [Data] = []

for (index, image) in images.enumerated() {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W3
withThrowingTaskGroup으로 작성하면 오류 처리를 일괄적으로 할 수 있어서 좋더라구요 !! 전에 프로젝트 할 때 사용했던 코드 첨부합니당!.!

    /// SwiftUI용으로 새로 나온 PhotosPicker 를 사용할 때 편하게 이미지를 업로드 할 수 있는 함수
    func postPhotosPickerItem(path: String, imageName: String, item: PhotosPickerItem) async throws -> String {
        guard let data = try? await item.loadTransferable(type: Data.self) else {
            throw FirebaseError.encodingFailed
        }
        
        let imagePath = "\(path)/\(imageName).jpg"
        return try await postData(path: imagePath, data: data)
    }
    
    /// 여러 PhotosPickerItem을 동시에 업로드 할때. post 코드는 병렬로 호출되어서 동시에 진행된다.
    func postPhotosPickerItems(path: String, imageNames: [String], items: [PhotosPickerItem]) async throws -> [String] {
        var results = Array(repeating: "", count: items.count)
        
        try await withThrowingTaskGroup(of: (Int, String).self) { group in
            for (index, item) in items.enumerated() {
                group.addTask {
                    let url = try await postPhotosPickerItem(path: path, imageName: imageNames[index], item: item)
                    return (index, url)
                }
            }
            
            for try await (index, url) in group {
                results[index] = url
            }
        }
        
        return results
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W3
요런 방식으로 공용 컴포넌트를 만들 수 있군용 . .! 좋네요!!

Comment on lines +52 to +54
group.notify(queue: .main) { [weak self] in
self?.didSelectImages?(images)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W3
신기하네여 . . group에 추가된 모든 작업이 끝나면 notify를 통해 원하는 처리를 해주는 늒김 .. ? 신기하네요

@Guryss Guryss merged commit 58a0747 into Develop Jun 4, 2025
@github-project-automation github-project-automation bot moved this from In Progress to Done in WSS-iOS Jun 4, 2025
@Guryss Guryss deleted the Feat/#554 branch June 4, 2025 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feat] 피드 작성뷰 내 이미지 첨부 UI 구현

4 participants