Skip to content

๐Ÿ–จ๏ธ EPSON Innovation Challenge ์ตœ์šฐ์ˆ˜์ƒ(1์œ„) ์ˆ˜์ƒ, 1000๋งŒ์› ์ƒ๊ธˆ

License

Notifications You must be signed in to change notification settings

Chillin-epson/iOS-Mobile

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

79 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

์น ํ•˜๋‹ค - ์ƒ์ƒํ•œ ๊ฒƒ์„ ์ƒ‰์น ํ•˜๊ณ  ํ”„๋ฆฐํŠธํ•˜๋‹ค.


ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

์•ฑ ์„ค๋ช…

  • ์น ํ•˜๋‹ค ์•ฑ์€ AI ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•˜์—ฌ ์•„์ด๊ฐ€ ์ƒ์ƒํ•œ ๊ทธ๋ฆผ์„ ์Œ์„ฑ์œผ๋กœ ์ž…๋ ฅํ•˜๋ฉด ํ•ด๋‹น ๋„์•ˆ์„ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ ํ”„๋ฆฐํŠธํ•˜์—ฌ ์ƒ‰์น  ๋†€์ด๋ฅผ ์ฆ๊ธธ ์ˆ˜ ์žˆ๋Š” ์•ฑ์ž…๋‹ˆ๋‹ค. ์ƒ‰์น ์ด ์™„๋ฃŒ๋œ ํ›„์—๋Š” ๋„์•ˆ์„ ์Šค์บ”ํ•˜์—ฌ, ์ƒ‰์น ํ•œ ์บ๋ฆญํ„ฐ์™€ ํ•จ๊ป˜ ์‚ฌ์ง„์„ ์ฐ์–ด ์ถ”์–ต์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ฑ๊ณผ

  • ์—ก์† Epson Innovation Challenge ์ตœ์šฐ์ˆ˜์ƒ(1์œ„) ์ˆ˜์ƒ, ์ƒ๊ธˆ 1,000๋งŒ ์›
  • ๊ตญ๋‚ด ์ตœ์ดˆ ๊ฐœ์ตœ ๋ฐ ์„ธ๊ณ„ 3๋ฒˆ์งธ๋กœ ์—ด๋ฆฐ ๊ธ€๋กœ๋ฒŒ ๋Œ€ํšŒ์—์„œ ์šฐ์Šน
  • ์ผ๋ณธ ๋ณธ์‚ฌ ์„ธ์ด์ฝ” ์—ก์†(Seiko EPSON Corporation) ์ตœ๊ณ ์šด์˜์ฑ…์ž„์ž(COO) Yoshida Junkichi๋กœ๋ถ€ํ„ฐ ๋ช…ํ•จ์„ ๋ฐ›๊ณ 
    ๊ธ€๋กœ๋ฒŒ ์•ฑ ์ถœ์‹œ๋ฅผ ์œ„ํ•œ ์ƒ๋ถ€ ํšŒ์˜ ์ง„ํ–‰
  • ํ•œ๊ตญ์—ก์† ๋Œ€ํ‘œ์ด์‚ฌ(CEO) Fujii Shigeo์™€ ๋„คํŠธ์›Œํ‚น
  • ์—ก์† EPSON๊ณผ ํŒŒํŠธ๋„ˆ์‰ฝ ์ฒด๊ฒฐ, ๊ธ€๋กœ๋ฒŒ ์•ฑ ๋Ÿฐ์นญ ์ง„ํ–‰์ค‘


ํ•œ๊ตญ์—ก์† ๋Œ€ํ‘œ์ด์‚ฌ(CEO) ํ›„์ง€์ด ์‹œ๊ฒŒ์˜ค


ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„

  • 2024.06.01 ~ 2024.6.29 (4์ฃผ)

ํ”„๋กœ์ ํŠธ ์ฐธ์—ฌ ์ธ์›

  • iOS Developer 1๋ช…, Back-end 2๋ช…, Design 1๋ช…, PM 1๋ช…

๊ธฐ์ˆ  ์Šคํƒ

  • Framework UIKit, Speech, Alamofire, Kingfisher, SnapKit, Photos, AVFoundation, Gifu, Lottie, AuthenticationServices

  • Design Pattern MVC

  • ์ „์ฒด ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜


ํ•ต์‹ฌ ๊ธฐ๋Šฅ๊ณผ ์ฝ”๋“œ ์„ค๋ช…

  • 1.์Œ์„ฑ์ธ์‹์„ ํ™œ์šฉํ•œ ํ…์ŠคํŠธ ์ถ”์ถœ ๋ฐ ๋„์•ˆ ์ƒ์„ฑ

  • 2.์ƒ์„ฑ๋œ ๋„์•ˆ ์ธ์‡„ํ•˜๊ธฐ(EPSON Connect API)

  • 3.์Šค์บ”ํ•œ ๋„์•ˆ์„ ์Šค์บ”ํ•จ์—์„œ ๊ด€๋ฆฌํ•˜๊ณ  ๊ฐค๋Ÿฌ๋ฆฌ์— ์ €์žฅ

  • 4.์Šค์บ”๋œ ์บ๋ฆญํ„ฐ์— ๋ชจ์…˜์„ ์ ์šฉํ•˜์—ฌ ์›€์ง์ด๊ฒŒ ๋งŒ๋“ค๊ธฐ

  • 5.์Šค์บ”๋œ ์บ๋ฆญํ„ฐ๋ฅผ ํ™œ์šฉํ•œ ์Šคํ‹ฐ์ปค ์‚ฌ์ง„ ์ดฌ์˜

  • 6.์• ํ”Œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

1.์Œ์„ฑ์ธ์‹์„ ํ™œ์šฉํ•œ ํ…์ŠคํŠธ ์ถ”์ถœ ๋ฐ ๋„์•ˆ ์ƒ์„ฑ

์น ํ•˜๋‹ค ์•ฑ์€ ์Œ์„ฑ์ธ์‹(Speech Recognition)์„ ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์Œ์„ฑ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ณ€ํ™˜๋œ ํ…์ŠคํŠธ๋Š” AI ๊ธฐ๋ฐ˜ ๋„์•ˆ ์ƒ์„ฑ API์™€ ์—ฐ๋™๋˜์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์ƒํ•œ ๋‚ด์šฉ์„ ๋„์•ˆ์œผ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค. ์Œ์„ฑ์ธ์‹์˜ ํšจ์œจ์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด iOS์˜ SFSpeechRecognizer๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ๋น„๋™๊ธฐ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์œผ๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.


  1. Speech ํ”„๋ ˆ์ž„ ์›Œํฌ ์‚ฌ์šฉ
  2. speechRecognizer: ํ•œ๊ตญ์–ด ์Œ์„ฑ ์ธ์‹์„ ๋‹ด๋‹น
  3. recognitionRequest: ์Œ์„ฑ ๋ฐ์ดํ„ฐ๋ฅผ SFSpeechRecognizer๋กœ ์ „๋‹ฌํ•˜๋Š” ์š”์ฒญ ๊ฐ์ฒด
  4. audioEngine: ๋งˆ์ดํฌ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌ
let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ko-KR"))!
var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
var recognitionTask: SFSpeechRecognitionTask?
let audioEngine = AVAudioEngine()

1-1. ์Œ์„ฑ์ธ์‹ ์‹œ์ž‘

  • ์‚ฌ์šฉ์ž๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์Œ์„ฑ ์ž…๋ ฅ์ด ์‹œ์ž‘๋˜๋ฉฐ audioEngine์ด ๋งˆ์ดํฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘
  • ์ธ์‹ ์ค‘๊ฐ„์— ๋ฒ„ํŠผ์„ ๋‹ค์‹œ ๋ˆ„๋ฅด๋ฉด ์Œ์„ฑ ์ž…๋ ฅ ์ข…๋ฃŒ
@objc func startRecording() {
    if audioEngine.isRunning {
        audioEngine.stop()
        recognitionRequest?.endAudio()
        voiceView.recordButton.isEnabled = false
    } else {
        startSpeechRecognition()
        voiceView.recordButton.setTitle("Stop", for: [])
        showBottomSheet()
    }
}

1-2. ์Œ์„ฑ ๋ฐ์ดํ„ฐ๋ฅผ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜

  • ์ž…๋ ฅ ๋ฐ์ดํ„ฐ: audioEngine์ด ๋งˆ์ดํฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•ด recognitionRequest๋กœ ์ „๋‹ฌ
  • ํ…์ŠคํŠธ ๋ณ€ํ™˜: ์Œ์„ฑ ๋ฐ์ดํ„ฐ๋Š” SFSpeechRecognizer๋กœ ๋ณ€ํ™˜๋˜๋ฉฐ, bestTranscription ์†์„ฑ์—์„œ ์ตœ์ ํ™”๋œ ํ…์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์˜ด
  • ์ค‘๊ฐ„๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ: shouldReportPartialResults๋ฅผ ํ™œ์„ฑํ™”ํ•˜์—ฌ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณ€ํ™˜๋œ ํ…์ŠคํŠธ ํ‘œ์‹œ
func startSpeechRecognition() {
    recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
    let inputNode = audioEngine.inputNode

    recognitionRequest?.shouldReportPartialResults = true

    recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest!) { (result, error) in
        if let result = result {
            self.recognizedText = result.bestTranscription.formattedString
            self.bottomSheetView.updateText(self.recognizedText)
        }
        if error != nil || result?.isFinal == true {
            self.audioEngine.stop()
            inputNode.removeTap(onBus: 0)
            self.recognitionRequest = nil
            self.recognitionTask = nil
        }
    }

    let recordingFormat = inputNode.outputFormat(forBus: 0)
    inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, when in
        self.recognitionRequest?.append(buffer)
    }
    audioEngine.prepare()
    try? audioEngine.start()
}

1-3. ๋ณ€ํ™˜๋œ ํ…์ŠคํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋„์•ˆ ์ƒ์„ฑ

  • ๋ณ€ํ™˜๋œ ํ…Œ์ŠคํŠธ๋ฅผ ์„œ๋ฒ„์— ์ „๋‹ฌ
  • Alamofire๋กœ ํ†ต์‹  ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ UI๊ฐ€ ๋ฉˆ์ถ”์ง€ ์•Š๋„๋ก ์„ค์ •
  • ๋„์•ˆ ์ƒ์„ฑ: ์„œ๋ฒ„์—์„œ ์ƒ์„ฑ๋œ ๋„์•ˆ์˜ URL๊ณผ ID๋ฅผ ๋ฐ˜ํ™˜
  • JSON ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ„ํŽธํ•˜๊ฒŒ ์„œ๋ฒ„๋กœ ์ „์†ก
  • ์ƒ์„ฑ๋œ ๋„์•ˆ ๊ฒฐ๊ณผ ํ‘œ์‹œ: ๋„์•ˆ์„ ๋กœ๋“œํ•˜์—ฌ ์ƒˆ๋กœ์šด ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹œ๊ฐํ™”
@objc func generateDrawing() {
    let prompt = voiceView.textView.text ?? ""
    let parameters: [String: Any] = ["prompt": prompt]

    AF.request("https://api.zionhann.com/chillin/drawings/gen", method: .post, parameters: parameters, encoding: JSONEncoding.default)
        .responseJSON { response in
            switch response.result {
            case .success(let value):
                if let json = value as? [String: Any],
                   let drawingId = json["drawingId"] as? Int,
                   let urlString = json["url"] as? String,
                   let url = URL(string: urlString) {
                    let createDrawingVC = CreateDrawingViewController()
                    createDrawingVC.loadImage(from: url)
                    createDrawingVC.drawingId = drawingId
                    self.navigationController?.pushViewController(createDrawingVC, animated: true)
                }
            case .failure(let error):
                print("Error: \(error)")
            }
        }
}

2.์ƒ์„ฑ๋œ ๋„์•ˆ ์ธ์‡„ํ•˜๊ธฐ(EPSON Connect API)

์น ํ•˜๋‹ค ์•ฑ์€ Epson Connect API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ƒ์„ฑ๋œ ๋„์•ˆ์„ ์ธ์‡„ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์ถœ๋ ฅ๋ฌผ์˜ ํฌ๊ธฐ๋ฅผ ์„ ํƒํ•œ ํ›„, ํ”„๋ฆฐํ„ฐ์™€์˜ ํ†ต์‹ ์„ ํ†ตํ•ด ์›ํ•˜๋Š” ํฌ๊ธฐ๋กœ ๋„์•ˆ์„ ์ธ์‡„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์€ ์‚ฌ์šฉ์ž์˜ ์„ ํƒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์„œ๋ฒ„์™€ ๋น„๋™๊ธฐ๋กœ ํ†ต์‹ ํ•˜๋ฉฐ, ์•ˆ์ •์ ์ธ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.


  1. Epson Connect API: Epson ํ”„๋ฆฐํ„ฐ์™€์˜ ํ†ต์‹ ์„ ํ†ตํ•ด ์ธ์‡„๋ฅผ ์ฒ˜๋ฆฌ
  2. Alamofire: ์„œ๋ฒ„์™€์˜ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ์ฒ˜๋ฆฌ
  3. UIAlertController: ์‚ฌ์šฉ์ž ํ™•์ธ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
  4. JSON Encoding: ๋ฐ์ดํ„ฐ ์ „์†ก์„ ์œ„ํ•œ JSON ํ˜•์‹ ๋ณ€ํ™˜

2-1. ํฌ๊ธฐ ์„ ํƒ ๋ฐ ์‚ฌ์šฉ์ž ํ™•์ธ

  • ์‚ฌ์šฉ์ž๊ฐ€ ์ถœ๋ ฅ๋ฌผ ํฌ๊ธฐ๋ฅผ ์„ ํƒํ•˜๋ฉด, ์„ ํƒ๋œ ํฌ๊ธฐ๊ฐ€ ์„œ๋ฒ„๋กœ ์ „๋‹ฌ๋˜์–ด ์ธ์‡„ ์ค€๋น„๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํฌ๊ธฐ ์„ ํƒ์€ ๋ฒ„ํŠผ์„ ํ†ตํ•ด ์ด๋ฃจ์–ด์ง€๋ฉฐ, ์„ ํƒ๋œ ํฌ๊ธฐ๋Š” selectedSize ๋ณ€์ˆ˜์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
@objc func sizeButtonTapped(_ sender: UIButton) {
   let buttons = [createDrawingView.largeSizeButton, createDrawingView.mediumSizeButton, createDrawingView.smallSizeButton]
   buttons.forEach { button in
       if button == sender {
           button.backgroundColor = .lightGray
           switch button {
           case createDrawingView.largeSizeButton:
               selectedSize = "LARGE"
           case createDrawingView.mediumSizeButton:
               selectedSize = "MEDIUM"
           case createDrawingView.smallSizeButton:
               selectedSize = "SMALL"
           default:
               selectedSize = "LARGE"
           }
       } else {
           button.backgroundColor = .white
       }
   }
}

2-2. ์„œ๋ฒ„๋กœ ์ธ์‡„ ์š”์ฒญ

  • Epson Connect API๋ฅผ ํ†ตํ•ด ๋„์•ˆ ID์™€ ํฌ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ JSON ํ˜•์‹์œผ๋กœ ์„œ๋ฒ„์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์š”์ฒญ์€ Alamofire๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ์„ฑ๊ณต ์—ฌ๋ถ€์— ๋”ฐ๋ผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฒฐ๊ณผ๋ฅผ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.
func printDrawing() {
    guard let drawingId = drawingId else { return }
    let parameters: [String: Any] = ["drawingId": drawingId, "scale": selectedSize]
    
    AF.request("https://api.zionhann.com/chillin/drawings/print", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: ["Content-Type": "application/json"]).responseJSON { response in
        if let statusCode = response.response?.statusCode {
            if statusCode == 200 {
                print("Print Success: \(response)")
                self.showPrintSuccessAlert()
            } else {
                print("Print Failure: \(response)")
                self.showPrintFailureAlert()
            }
        } else {
            print("Error: \(response.error ?? AFError.explicitlyCancelled)")
            self.showPrintFailureAlert()
        }
        self.createDrawingView.printSuccessReturnUI(false)
    }
}

3. ์Šค์บ”ํ•œ ๋„์•ˆ์„ ์Šค์บ”ํ•จ์—์„œ ๊ด€๋ฆฌํ•˜๊ณ  ๊ฐค๋Ÿฌ๋ฆฌ์— ์ €์žฅ

  • ์น ํ•˜๋‹ค ์•ฑ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์Šค์บ”ํ•œ ๋„์•ˆ์„ ์Šค์บ”ํ•จ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ ์„œ๋ฒ„์™€์˜ ํ†ต์‹ ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑํ•œ ๋„์•ˆ์„ ๋ถˆ๋Ÿฌ์™€ ๊น”๋”ํ•œ UI๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ํ†ตํ•ด ํ•œ ๋ฒˆ์— ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ์•Š๊ณ , ์Šคํฌ๋กค ์‹œ ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํšจ์œจ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.


  1. Alamofire: ์„œ๋ฒ„์—์„œ ์Šค์บ” ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๋™๊ธฐ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ
  2. UICollectionView: ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ๋„์•ˆ์„ ์ •๋ ฌ ๋ฐ ํ‘œ์‹œ
  3. Kingfisher: ์„œ๋ฒ„์—์„œ ๋ฐ›์€ ์ด๋ฏธ์ง€ URL์„ ๋น ๋ฅด๊ฒŒ ๋ Œ๋”๋ง
  4. Pagination: ๋ฐ์ดํ„ฐ ์š”์ฒญ์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ

3-1. ์Šค์บ”ํ•จ์˜ ๋„์•ˆ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

  • ์Šค์บ”ํ•จ์— ์ €์žฅ๋œ ๋„์•ˆ์„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„์— GET ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ์š”์ฒญ ์‹œ, ํ˜„์žฌ ํŽ˜์ด์ง€ ์ •๋ณด์™€ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ธฐ ์œ„ํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋™์•ˆ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ, ์„œ๋ฒ„ ์‘๋‹ต์— ๋”ฐ๋ผ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
func fetchDrawings(page: Int) {
    guard !isLoading, hasMoreData else { return }
    isLoading = true
    
    let url = "https://api.zionhann.com/chillin/drawings"
    let parameters: [String: Any] = ["type": "GENERATED", "page": page]
    
    AF.request(url, method: .get, parameters: parameters).responseJSON { response in
        self.isLoading = false
        switch response.result {
        case .success(let value):
            if let json = value as? [String: Any], let data = json["data"] as? [[String: Any]] {
                let newDrawings = data.compactMap { Drawing(dictionary: $0) }
                if newDrawings.isEmpty {
                    self.hasMoreData = false
                } else {
                    self.drawings.append(contentsOf: newDrawings)
                    self.scanView.collectionView.reloadData()
                }
            }
        case .failure(let error):
            print("Error: \(error)")
        }
    }
}

3-2. ์Šค์บ” ๋„์•ˆ์˜ UI ํ‘œ์‹œ

  • ๋ถˆ๋Ÿฌ์˜จ ๋„์•ˆ ๋ฐ์ดํ„ฐ๋ฅผ UICollectionView๋ฅผ ์‚ฌ์šฉํ•ด ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋„์•ˆ์€ CollectionViewCell์— ์ด๋ฏธ์ง€์™€ ํ•จ๊ป˜ ๊น”๋”ํ•˜๊ฒŒ ๋ Œ๋”๋ง๋˜๋ฉฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์Šค์บ”ํ•จ์˜ ๋„์•ˆ์„ ์‰ฝ๊ฒŒ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return drawings.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
    let drawing = drawings[indexPath.item]
    cell.configure(with: drawing)
    return cell
}

3-3. ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„

  • ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ๊ฐ์ง€ํ•ด ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กค์„ ๋‚ด๋ ค ๋์— ๋„๋‹ฌํ•˜๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์—์„œ ์š”์ฒญํ•˜๊ณ , ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ‘ํ•ฉํ•˜์—ฌ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offsetY = scrollView.contentOffset.y
    let contentHeight = scrollView.contentSize.height
    let height = scrollView.frame.size.height

    if offsetY > contentHeight - height * 2 {
        currentPage += 1
        fetchDrawings(page: currentPage)
    }
}

3-4. ์Šค์บ” ๋„์•ˆ ์ƒ์„ธ๋ณด๊ธฐ ๋ฐ ์ €์žฅํ•˜๊ธฐ

  • ์Šค์บ”ํ•จ์—์„œ ๋„์•ˆ์„ ์„ ํƒํ•˜๋ฉด, ํ•ด๋‹น ๋„์•ˆ์˜ ์ƒ์„ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ์ƒ์„ธ ํ™”๋ฉด์—์„œ๋Š” ์„ ํƒํ•œ ๋„์•ˆ์„ ํ™•๋Œ€ํ•˜๊ฑฐ๋‚˜, ์ธ์‡„ ๋ฐ ์ €์žฅ๊ณผ ๊ฐ™์€ ์ถ”๊ฐ€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • 1. Photos Framework ํ™œ์šฉ

  • PHPhotoLibrary๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ ‘๊ทผ ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๊ณ , ์ €์žฅ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  • 2. ๊ถŒํ•œ ์š”์ฒญ ๋ฐ ์ด๋ฏธ์ง€ ์ €์žฅ

  • ์‚ฌ์šฉ์ž๊ฐ€ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•˜๋ฉด, ์„ ํƒํ•œ ๋„์•ˆ ์ด๋ฏธ์ง€๋ฅผ ๊ฐค๋Ÿฌ๋ฆฌ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

  • ์ €์žฅ ์™„๋ฃŒ ํ›„ ์•Œ๋ฆผ ์ฐฝ์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let drawing = drawings[indexPath.item]
    let scanCheckVC = ScanCheckViewController()
    scanCheckVC.drawing = drawing
    scanCheckVC.drawingId = drawing.drawingId
    
    let scanCheckNaviController = UINavigationController(rootViewController: scanCheckVC)
    scanCheckNaviController.modalPresentationStyle = .overFullScreen
    self.present(scanCheckNaviController, animated: true, completion: nil)
}

@objc func saveImageButtonTapped() {
    guard let image = scanCheckView.resultImageView.image else { return }
    
    PHPhotoLibrary.requestAuthorization { status in
        if status == .authorized {
            UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
        } else {
            print("Authorization denied")
        }
    }
}

@objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
    if let error = error {
        print("Error saving image: \(error.localizedDescription)")
    } else {
        print("Image saved successfully")
        let alert = UIAlertController(title: "์ €์žฅ ์™„๋ฃŒ", message: "์ด๋ฏธ์ง€๊ฐ€ ๊ฐค๋Ÿฌ๋ฆฌ์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "ํ™•์ธ", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }
}

4. ์Šค์บ”๋œ ์บ๋ฆญํ„ฐ์— ๋ชจ์…˜์„ ์ ์šฉํ•˜์—ฌ ์›€์ง์ด๊ฒŒ ๋งŒ๋“ค๊ธฐ

  • ์‚ฌ์šฉ์ž๋Š” ์Šค์บ”๋œ ์บ๋ฆญํ„ฐ๋ฅผ ์„ ํƒํ•˜์—ฌ ๋‹ค์–‘ํ•œ ๋ชจ์…˜ ํšจ๊ณผ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ƒ์„ฑ๋œ ์• ๋‹ˆ๋ฉ”์ด์…˜์€ GIF๋กœ ์ €์žฅํ•˜์—ฌ ๊ฐ„์งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์บ๋ฆญํ„ฐ์™€ ๋ชจ์…˜์˜ ์กฐํ•ฉ์œผ๋กœ ์ฐฝ์˜์ ์ด๊ณ  ์žฌ๋ฏธ์žˆ๋Š” ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

4-1. ์บ๋ฆญํ„ฐ ์„ ํƒ ๋ฐ ๋ชจ์…˜ ํƒ€์ž… ์ง€์ •

  • ์‚ฌ์šฉ์ž๋Š” UICollectionView๋ฅผ ํ†ตํ•ด ์Šค์บ”๋œ ์บ๋ฆญํ„ฐ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
  • ์ดํ›„ ๋ชจ์…˜ ํƒ€์ž… ๋ฒ„ํŠผ(๋Œ„์Šค, ์•ˆ๋…•, ์ ํ”„, ์ข€๋น„ ๋“ฑ) ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•˜์—ฌ ์›€์ง์ž„์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
  • ์„ ํƒ๋œ ๋ชจ์…˜ ํƒ€์ž…์€ ์„œ๋ฒ„ ์š”์ฒญ ์‹œ ์ „๋‹ฌ๋˜์–ด ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.
@objc func motionButtonTapped(_ sender: UIButton) {
   let buttons = [motionCheckView.danceButton, motionCheckView.helloButton, motionCheckView.jumpButton, motionCheckView.zombieButton]
   buttons.forEach { button in
       if button == sender {
           button.backgroundColor = .lightGray
           switch button {
           case motionCheckView.danceButton:
               motionSelected = "dance"
           case motionCheckView.helloButton:
               motionSelected = "hello"
           case motionCheckView.jumpButton:
               motionSelected = "jump"
           case motionCheckView.zombieButton:
               motionSelected = "zombie"
           default:
               break
           }
       } else {
           button.backgroundColor = .white
       }
   }
}

4-2. ์„œ๋ฒ„ ์š”์ฒญ์„ ํ†ตํ•ด GIF ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ

  • Alamofire๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„ ํƒ๋œ ์บ๋ฆญํ„ฐ์™€ ๋ชจ์…˜ ํƒ€์ž…์„ ์„œ๋ฒ„์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  • ์„œ๋ฒ„๋Š” ์š”์ฒญ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ GIF ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ , ๊ฒฐ๊ณผ URL์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
@objc func motionStartButtonTapped() {
    guard let drawing = drawing else { return }
    guard let motionSelected = motionSelected else { return }

    let url = "https://api.zionhann.com/chillin/motion/\(drawing.drawingId)"
    let parameters: [String: Any] = ["motionType": motionSelected]

    AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseJSON { response in
        switch response.result {
        case .success(let value):
            if let json = value as? [String: Any], let gifUrlString = json["url"] as? String, let gifUrl = URL(string: gifUrlString) {
                self.presentMotionResultViewController(with: gifUrl)
            } else {
                self.showAlert(message: "ํ•ด๋‹น ๊ทธ๋ฆผ์€ ์›€์ง์ด๊ฒŒ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค!")
            }
        case .failure:
            self.showAlert(message: "๋ชจ์…˜ ์ ์šฉ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.")
        }
    }
}
  

4-3. GIF ๊ฒฐ๊ณผ ๋ณด๊ธฐ ๋ฐ ์ €์žฅ

  • ์„œ๋ฒ„์—์„œ ๋ฐ˜ํ™˜๋œ GIF URL์„ ์‚ฌ์šฉํ•ด Gifu ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ UI์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
  • GIF ํŒŒ์ผ์€ Photos Framework๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์˜ ๊ฐค๋Ÿฌ๋ฆฌ์— ์ €์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅ ํ›„์—๋Š” ์•Œ๋ฆผ์„ ํ†ตํ•ด ์ €์žฅ ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
func saveGifToLibrary(data: Data) {
    PHPhotoLibrary.requestAuthorization { status in
        guard status == .authorized else { return }
        PHPhotoLibrary.shared().performChanges({
            let options = PHAssetResourceCreationOptions()
            let creationRequest = PHAssetCreationRequest.forAsset()
            creationRequest.addResource(with: .photo, data: data, options: options)
        }) { success, error in
            if success {
                self.showSaveSuccessAlert()
            } else {
                self.showSaveErrorAlert(error: error)
            }
        }
    }
}

5. ์Šค์บ”๋œ ์บ๋ฆญํ„ฐ๋ฅผ ํ™œ์šฉํ•œ ์Šคํ‹ฐ์ปค ์‚ฌ์ง„ ์ดฌ์˜

  • ์Šค์บ”๋œ ์บ๋ฆญํ„ฐ๋ฅผ ์‚ฌ์ง„ ์ดฌ์˜์— ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ๋งž์ถคํ˜• ์Šคํ‹ฐ์ปค ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž๋Š” ์นด๋ฉ”๋ผ ํ™”๋ฉด์— ์บ๋ฆญํ„ฐ ์ด๋ฏธ์ง€๋ฅผ ์˜ค๋ฒ„๋ ˆ์ดํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒํƒœ์—์„œ ์ดฌ์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ดฌ์˜๋œ ์ด๋ฏธ์ง€๋Š” ์Šคํ‹ฐ์ปค๊ฐ€ ํฌํ•จ๋œ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ์ž ๊ฐค๋Ÿฌ๋ฆฌ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

5-1. ์นด๋ฉ”๋ผ ์˜ค๋ฒ„๋ ˆ์ด ์„ค์ •

  • ์Šค์บ”๋œ ์บ๋ฆญํ„ฐ ์ด๋ฏธ์ง€๋ฅผ ์นด๋ฉ”๋ผ ํ™”๋ฉด์— ์˜ค๋ฒ„๋ ˆ์ดํ•˜์—ฌ ์Šคํ‹ฐ์ปค์ฒ˜๋Ÿผ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž๋Š” ๋“œ๋ž˜๊ทธ(์ด๋™)์™€ ํ•€์น˜ ์ œ์Šค์ฒ˜(ํฌ๊ธฐ ์กฐ์ ˆ)๋ฅผ ํ†ตํ•ด ์Šคํ‹ฐ์ปค์˜ ์œ„์น˜์™€ ํฌ๊ธฐ๋ฅผ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
func createOverlayView() -> UIView {
    let overlayView = UIView(frame: view.bounds)
    overlayView.backgroundColor = .clear
    overlayView.isUserInteractionEnabled = true

    let removeImageViewFrame = motionCheckView.convert(motionCheckView.removeImageView.frame, to: overlayView)
    let removeImageView = UIImageView(frame: removeImageViewFrame)
    removeImageView.image = motionCheckView.removeImageView.image
    removeImageView.contentMode = .scaleAspectFill
    removeImageView.isUserInteractionEnabled = true

    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
    removeImageView.addGestureRecognizer(panGesture)

    let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
    removeImageView.addGestureRecognizer(pinchGesture)

    overlayView.addSubview(removeImageView)
    return overlayView
}

@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
    guard let view = gesture.view else { return }
    let translation = gesture.translation(in: view.superview)
    view.center = CGPoint(x: view.center.x + translation.x, y: view.center.y + translation.y)
    gesture.setTranslation(.zero, in: view.superview)
}

@objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
    guard let view = gesture.view else { return }
    view.transform = view.transform.scaledBy(x: gesture.scale, y: gesture.scale)
    gesture.scale = 1.0
}

5-2. ์‚ฌ์ง„ ์ดฌ์˜ ๋ฐ ์Šคํ‹ฐ์ปค ํฌํ•จ ์ด๋ฏธ์ง€ ์ƒ์„ฑ

  • ์‚ฌ์ง„ ์ดฌ์˜ ์‹œ ์˜ค๋ฒ„๋ ˆ์ด๋œ ์Šคํ‹ฐ์ปค๋ฅผ ํฌํ•จํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€๋Š” ์Šคํ‹ฐ์ปค๊ฐ€ ์‚ฌ์ง„ ์œ„์— ๋ฐฐ์น˜๋œ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ์ž ๊ฐค๋Ÿฌ๋ฆฌ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
@objc func shutterButtonTapped() {
    if let imagePickerController = self.presentedViewController as? UIImagePickerController {
        imagePickerController.takePicture()
    }
}

func takeSnapshotWithOverlayAndSave(capturedImage: UIImage, isFrontCamera: Bool) {
    var imageToSave = capturedImage
    if isFrontCamera {
        imageToSave = UIImage(cgImage: capturedImage.cgImage!, scale: capturedImage.scale, orientation: .leftMirrored)
    }

    UIGraphicsBeginImageContextWithOptions(imageToSave.size, false, imageToSave.scale)
    imageToSave.draw(in: CGRect(origin: .zero, size: imageToSave.size))

    if let overlayImage = motionCheckView.removeImageView.image {
        overlayImage.draw(in: CGRect(x: 100, y: 100, width: 200, height: 200))
    }

    let combinedImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    if let combinedImage = combinedImage {
        UIImageWriteToSavedPhotosAlbum(combinedImage, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
    }
}

@objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
    if let error = error {
        print("Error saving image: \(error.localizedDescription)")
    } else {
        print("Image saved successfully")
    }
}

5-3. ์ปค์Šคํ…€ ์นด๋ฉ”๋ผ ์ธํ„ฐํŽ˜์ด์Šค

  • ๊ธฐ๋ณธ ์นด๋ฉ”๋ผ UI๋ฅผ ์ˆจ๊ธฐ๊ณ  ์ปค์Šคํ…€ ๋ฒ„ํŠผ(์ดฌ์˜, ์ „ํ™˜, ์ทจ์†Œ)์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.
  • ์นด๋ฉ”๋ผ ํ™”๋ฉด์€ ์ •์‚ฌ๊ฐํ˜• ๋น„์œจ๋กœ ์กฐ์ •๋˜์–ด ์Šคํ‹ฐ์ปค ์‚ฌ์ง„ ์ดฌ์˜์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
func presentCameraWithOverlay() {
   let cameraVC = UIImagePickerController()
   cameraVC.delegate = self
   cameraVC.sourceType = .camera
   cameraVC.cameraOverlayView = createOverlayView()
   cameraVC.showsCameraControls = false

   let scale = UIScreen.main.bounds.width / UIScreen.main.bounds.width
   cameraVC.cameraViewTransform = CGAffineTransform(scaleX: scale, y: scale)

   present(cameraVC, animated: true, completion: nil)
}

6. ์• ํ”Œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

  • ์น ํ•˜๋‹ค ์•ฑ์€ Apple Sign-In ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์• ํ”Œ ๊ณ„์ •์„ ํ™œ์šฉํ•ด ์•ฑ์— ๊ฐ„ํŽธํ•˜๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ AuthenticationServices ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ™œ์šฉํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ณดํ˜ธํ•˜๋ฉด์„œ ์›ํ™œํ•œ ์ธ์ฆ ๊ณผ์ •์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  1. ์• ํ”Œ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ์ œ๊ณต
  • ASAuthorizationAppleIDButton์„ ํ™œ์šฉํ•œ ์ง๊ด€์ ์ด๊ณ  ๊น”๋”ํ•œ UI ๊ตฌ์„ฑ
  • ์‚ฌ์šฉ์ž์—๊ฒŒ ์• ํ”Œ ๊ณ„์ •์„ ํ†ตํ•ด ๊ฐ„ํŽธํ•œ ๋กœ๊ทธ์ธ ์˜ต์…˜ ์ œ๊ณต

  1. ์ธ์ฆ ์š”์ฒญ ๋ฐ ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
  • ์‚ฌ์šฉ์ž์˜ ์ด๋ฆ„, ์ด๋ฉ”์ผ, ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ˆ˜์ง‘ํ•˜์—ฌ UserDefaults์™€ ์„œ๋ฒ„์— ์ €์žฅ
  • ์ˆ˜์ง‘๋œ ์ •๋ณด๋Š” ํ–ฅํ›„ ์ž๋™ ๋กœ๊ทธ์ธ ๋ฐ ๊ฐœ์ธํ™”๋œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ๋ฐ ํ™œ์šฉ

  1. ์ž๋™ ๋กœ๊ทธ์ธ
  • ์ด์ „ ๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์•ฑ ์‹คํ–‰ ์‹œ ์ž๋™์œผ๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ ์ˆ˜ํ–‰
  • ์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ

  1. ์„œ๋ฒ„ ํ†ต์‹  ๋ฐ ํ† ํฐ ๊ด€๋ฆฌ
  • Alamofire๋ฅผ ์‚ฌ์šฉํ•ด ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜์—ฌ ์ธ์ฆ ํ† ํฐ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
  • ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ํ† ํฐ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž ์„ธ์…˜ ๊ด€๋ฆฌ

  1. ๋กœ๊ทธ์ธ ์‹คํŒจ ์ฒ˜๋ฆฌ
  • ๋‹ค์–‘ํ•œ ์˜ค๋ฅ˜ ์ฝ”๋“œ์— ๋”ฐ๋ผ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ ์ ˆํ•œ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต
  • ๋กœ๊ทธ์ธ ์žฌ์‹œ๋„ ์˜ต์…˜๊ณผ ๊ด€๋ จ ์ •๋ณด๋ฅผ ์•ˆ๋‚ด


  1. ์• ํ”Œ ์ธ์ฆ ์š”์ฒญ ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ASAuthorizationController๋ฅผ ํ†ตํ•ด ์• ํ”Œ ์ธ์ฆ ์š”์ฒญ์„ ์ˆ˜ํ–‰
@objc func appleLoginButtonTapped() {
   let appleIDProvider = ASAuthorizationAppleIDProvider()
   let request = appleIDProvider.createRequest()
   request.requestedScopes = [.fullName, .email]
   
   let controller = ASAuthorizationController(authorizationRequests: [request])
   controller.delegate = self
   controller.presentationContextProvider = self
   controller.performRequests()
}

  1. ์ธ์ฆ ์„ฑ๊ณต ์‹œ ์‚ฌ์šฉ์ž ์ •๋ณด ์ฒ˜๋ฆฌ ์• ํ”Œ ๊ณ„์ • ์ธ์ฆ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๋ฉด, ์‚ฌ์šฉ์ž์˜ ์ด๋ฆ„, ์ด๋ฉ”์ผ, ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ด ์ •๋ณด๋Š” ์„œ๋ฒ„์™€์˜ ํ†ต์‹ ์— ์‚ฌ์šฉ๋˜๋ฉฐ, UserDefaults์— ์ €์žฅ
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
   guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else { return }
   
   let userIdentifier = appleIDCredential.user
   let email = appleIDCredential.email ?? "Email not provided"
   let fullName = appleIDCredential.fullName?.formatted() ?? "Name not provided"

   // ์„œ๋ฒ„๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด ์ „์†ก
   let parameters: [String: Any] = ["code": String(data: appleIDCredential.authorizationCode!, encoding: .utf8) ?? ""]
   let headers: HTTPHeaders = ["Content-Type": "application/json"]

   AF.request("https://api.zionhann.com/chillin/auth/oauth2/token", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
       switch response.result {
       case .success(let value):
           print("Authentication Successful: \(value)")
       case .failure(let error):
           print("Authentication Failed: \(error.localizedDescription)")
       }
   }

   UserDefaults.standard.set(userIdentifier, forKey: "User")
}

  1. ์ธ์ฆ ์‹คํŒจ ์ฒ˜๋ฆฌ ์ธ์ฆ ๊ณผ์ • ์ค‘ ์‹คํŒจ ์‹œ, ์‚ฌ์šฉ์ž์—๊ฒŒ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์ ์ ˆํ•œ ๋Œ€์ฒ˜ ๋ฐฉ๋ฒ•์„ ์•ˆ๋‚ด
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
   if let authorizationError = error as? ASAuthorizationError {
       switch authorizationError.code {
       case .canceled:
           print("User canceled the login request.")
       case .failed:
           print("Authorization request failed.")
       case .invalidResponse:
           print("Invalid response received.")
       default:
           print("Unknown error occurred.")
       }
   }
}

License and Copyright

ยฉ 2024 ์ด์Šนํ˜„ (SeungHyeon Lee). All rights reserved.

  • ๋ณธ GitHub ๋ฆฌ๋“œ๋ฏธ๋Š” ํŒ€ ํ”„๋กœ์ ํŠธ ์น ํ•˜๋‹ค๋ฅผ ์†Œ๊ฐœํ•˜๊ธฐ ์œ„ํ•ด ์ž‘์„ฑ๋˜์—ˆ์œผ๋ฉฐ, ๋ฆฌ๋“œ๋ฏธ์˜ ๋ชจ๋“  ๋‚ด์šฉ์€ ์ด์Šนํ˜„(SeungHyeon Lee)์ด ์ง์ ‘ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • ์ด ํ”„๋กœ์ ํŠธ๋Š” iOS Developer 1๋ช…, Back-end 2๋ช…, PM 1๋ช…์œผ๋กœ ๊ตฌ์„ฑ๋œ ํŒ€์ด ํ˜‘๋ ฅํ•˜์—ฌ ์ง„ํ–‰ํ•œ ๊ฒฐ๊ณผ๋ฌผ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋ฆฌ๋“œ๋ฏธ์— ํฌํ•จ๋œ ๋ชจ๋“  ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€ ๋ฐฐ์น˜, ์„ค๋ช…, ๊ธฐ์ˆ  ์Šคํƒ ์†Œ๊ฐœ ๋“ฑ์€ ์ €์˜ ์ž‘์—…์ž„์„ ๋ฐํž™๋‹ˆ๋‹ค.

  • ๋ฆฌ๋“œ๋ฏธ์™€ ๊ด€๋ จํ•˜์—ฌ ๋ฌธ์˜๊ฐ€ ํ•„์š”ํ•˜์‹  ๊ฒฝ์šฐ ์•„๋ž˜ ์ด๋ฉ”์ผ๋กœ ์—ฐ๋ฝํ•ด ์ฃผ์„ธ์š”: [email protected]

About

๐Ÿ–จ๏ธ EPSON Innovation Challenge ์ตœ์šฐ์ˆ˜์ƒ(1์œ„) ์ˆ˜์ƒ, 1000๋งŒ์› ์ƒ๊ธˆ

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages