iOS 앱에서 중복된 이름 저장에 대한 대처 방법(넘버링)

문인범·2025년 4월 12일

Macro 챌린지

목록 보기
5/6
post-thumbnail

저희 Reazy 앱은 논문을 업로드하면 해당 pdf 파일을 앱 Sandbox 내에 있는 Document Container에 저장하고 해당 url의 bookmark 데이터를 CoreData에 저장하고 사용하는 방식으로 구성이 되어있습니다.

이 과정에서 중복된 이름의 파일이 업로드가 되는 케이스가 있습니다.

  • 기존에 업로드 되어 있는 파일과 똑같은 이름으로 업로드를 할 경우
  • 특정 파일을 복제할 경우
  • 파일의 이름을 변경 시 중복된 이름으로 변경할 경우

마지막 케이스는 중복된 파일 이름이라고 알려주고 다시 이름을 수정하도록 하면 됩니다.

하지만 나머지 케이스 들은 업로드를 막기만 할 경우 적합하지 않은 사용자 경험을 제공할 가능성이 높습니다.
기존에 저희가 사용하던 서비스는 보통 중복된 타이틀이 들어가면 다운로드를 막지 않고 뒤에 넘버링을 붙여 다운로드를 막지 않게 하는게 자연스러운 흐름이기 때문입니다.

그래서 Reazy 앱에서도 해당 기능을 구현해보고자 했습니다.

(이런 기능은 자동으로 들어가 있을 것이라 생각한 제가 바보입니다 ㅎㅎ;;)

구현 아이디어

처음 생각한 방식은 되게 쉽게 생각했습니다.

// in HomeViewUseCase.swift

internal func savePDFIntoDirectory(url: URL, isSample: Bool) throws -> (Data, URL)? {
    do {
        let manager = FileManager.default
        let documentURL = manager.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentURL.appending(path: url.lastPathComponent)
        
        // 1. 저장 파일 중복 확인
        if let _ = try? Data(contentsOf: fileURL) {
            var dupNum = 1
            
            // 2. 파일 이름 뽑아오기
            var lastComponent = url.lastPathComponent.split(separator: ".")
            lastComponent.removeLast()
            
            
            while dupNum < 100 {
		            
		            // 3. 뒤에 숫자를 붙여 새로운 파일 URL 생성
                let tempURL = documentURL.appending(path: lastComponent.joined() + "(\(dupNum)).pdf")
                
                // 4. 중복된 파일 URL 판단, 중복이 아닐 경우 저장 진행
                guard let _ = try? Data(contentsOf: tempURL) else {
                    try manager.copyItem(at: url, to: tempURL)
                    return try (tempURL.bookmarkData(options: .minimalBookmark), tempURL)
                }
                
                if dupNum == 99 {
                    throw PDFUploadError.fileNameDuplication
                }
                
                // 중복 숫자에 +1을 하고 위 과정 다시 진행
                dupNum += 1
            }
            
            
        } else {
            try manager.copyItem(at: url, to: fileURL)
        }
        
        let urlData = try fileURL.bookmarkData(options: .minimalBookmark)
        
        return (urlData, fileURL)
    }
    ...
}

흐름도는 이러합니다

  1. Data 메소드를 통해 저장하려고 하는 URL에 파일이 존재하는지 확인(있으면 중복된 것으로 판단)
    1. 없을 경우 그대로 저장 진행
    2. 있을 경우 다음 순서 진행
  2. URL의 last component를 뽑아 파일 이름을 보관
  3. 보관한 파일 이름에 dupNum(숫자)을 붙여 새로운 URL 생성
    ex) paper.pdfpaper(1).pdf
  4. 해당 파일의 중복 여부를 확인
    1. 중복이 되어 있을 경우 dupNum을 +1 하고 다시 반복
    2. 아닐 경우 저장 진행

이렇게 구현하고 테스트를 해보니 아주 자연스럽게 잘 붙었습니다.
하지만 특정 상황에서 넘버링이 이상하게 붙는 현상이 발생했습니다.

  • 넘버링이 된 파일을 복제할 경우 → 숫자가 뒤에 새로 붙어서 시작함
    ex) Some Paper(1).pdf → Some Paper(1)(1).pdf

넘버링이 붙어 있는 파일을 인식하여 해당 넘버링을 뺀 후 중복 여부를 판단해야 하는데
숫자가 붙어있는 채로 중복여부를 판단하여 통과하고 뒤에 다시 넘버링이 붙은 이상한 상황이 되어버렸습니다.

어떻게 보면 시키는 대로 일을 하긴 했는데 좀 짜치는? 그런 기능이 되어 버려서 기존에 구상한 대로 고쳐보기로 했습니다.

수정본

파일 이름이 중복되어 있을 경우 정규 표현식을 활용하여 해당 파일에 넘버링 여부를 판단하는 기능을 추가로 넣어 해결했습니다.

// in HomeViewUseCase.swift

// 이전 메소드에서 중복 여부를 판단하고 중복되어 있을 경우 해당 메소드 실행
internal func copyItem(url: URL) -> (Data, URL)? {
    do {
        let manager = FileManager.default
        let documentURL = manager.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentURL.appending(path: url.lastPathComponent)
        
        let lastComponent = url.deletingPathExtension().lastPathComponent
        var splitComp = lastComponent.split(separator: " ")
        
        // 1. 넘버링 여부를 판단할 정규 표현식 생성
        let regex = Regex {
            "("
            OneOrMore(.digit)
            ")"
        }
        
        // 2. 정규표현식을 사용해 해당 URL에 넘버링이 되어있는지 판단
        // 공백을 기준으로 URL을 split 한 후 마지막 인덱스에 prefixMatch를 사용해 해당 정규표현식으로 표현이 되어있는지를 판단함
        if let splitCompLast = splitComp.last?.prefixMatch(of: regex) {
		        // 2-1. 정규표현식 결과물에 앞 뒤 괄호를 빼 넘버링 숫자를 찾음
            var fileNumString = splitCompLast.output
            fileNumString.removeFirst()
            fileNumString.removeLast()
            
            // 2-2. 넘버링을 뺀 URL을 생성
            splitComp.removeLast()
            let fileName = splitComp.joined(separator: " ")
            
            var fileNum = Int(fileNumString)!
            
            while(fileNum < 9999) {
		            // 2-3. 기존 넘버링에 +1하여 새로운 URL 생성
                let resultURL = documentURL.appending(path: fileName + " (\(fileNum+1)).pdf")
                
                // 2-4. 중복 여부 판단
                if let _ = try? Data(contentsOf: resultURL) {
		                // 2-5. 중복일 경우 +1, 다시 반복
                    fileNum += 1
                    continue
                }
                
                // 2-5. 중복이 아닐 경우 저장 진행
                try manager.copyItem(at: url, to: resultURL)
                let bookmarkData = try resultURL.bookmarkData(options: .suitableForBookmarkFile)
                return (bookmarkData, resultURL)
            }
        } else { // 3. 파일 이름 뒤에 넘버링이 붙어 있지 않은 경우
            var num = 1
            while(num < 9999) {
		            // 3-1. 넘버링이 붙어 있지 않으므로 1을 붙여 URL 생성
                let resultURL = documentURL.appending(path: lastComponent + " (\(num)).pdf")
                
                // 3-2. 중복여부 판단
                if let _ = try? Data(contentsOf: resultURL) {
		                // 3-3. 중복일 경우 +1
                    num += 1
                    continue
                }
                
                // 3-3. 중복이 아닐 경우 저장 진행
                try manager.copyItem(at: fileURL, to: resultURL)
                let urlData = try resultURL.bookmarkData(options: .suitableForBookmarkFile)
                
                return (urlData, resultURL)
            }
        }
    }
    ...
}

바깥 메소드에서 URL의 중복 여부를 판단한 뒤 중복일 경우 위 메소드가 실행되게 변경

  1. 넘버링 여부를 판단할 정규 표현식을 생성(RegexBuilder 사용)
  2. 정규표현식을 사용해 해당 URL에 넘버링이 되어있는지 판단함
    넘버링이 포함된 경우
    1. 정규표현식으로 추출한 문자열에서 괄호를 빼 숫자 보관
    2. 넘버링을 뺀 URL 생성
    3. 위 URL에 보관된 숫자에 +1을 하여 새로운 URL 생성
    4. 해당 URL의 중복여부를 판단하고 중복일 경우 위 과정 반복
    아닐 경우 저장 진행
  3. 넘버링이 포함되어 있지 않은 경우
    1. 뒤에 넘버링을 붙여 URL 생성(1부터)
    2. 중복 여부 판단
    3. 중복일 경우 +1을 하고 반복
      아닐 경우 저장 진행

이렇게 구현 방법을 바꾸어 넘버링을 깔끔하게 붙일 수 있었습니다!
개발을 하다보면 계속 느끼는 것인데 저희가 사용하면서 당연하다고 생각되는 것들이 막상 개발을 하면 당연한 것이 아니더군요........
개발자에 노고에 항상 감사를 드리면서 살아야겠습니다 ㅠㅠ

profile
월클 개발자를 향한 도전일지

0개의 댓글