본문 바로가기

iOS

iOS_3grade - 13주차

Gemini API(generateContent)를 Swift에서 사용하기 위한 JSON 모델을 설계

Request Model

import Foundation

// MARK: - Request

struct GeminiRequest: Codable {
    let contents: [Content]
    let systemInstruction: Content?
    let generationConfig: GenerationConfig?
    let safetySettings: [SafetySetting]?
    
    enum CodingKeys: String, CodingKey {
        case contents
        case systemInstruction = "system_instruction"
        case generationConfig = "generationConfig"
        case safetySettings = "safetySettings"
    }
}

struct Content: Codable {
    let role: String?
    let parts: [Part]
}

struct Part: Codable {
    let text: String?
    let inlineData: InlineData?

    enum CodingKeys: String, CodingKey {
        case text
        case inlineData = "inline_data"
    }
}

struct InlineData: Codable {
    let mimeType: String
    let data: String

    enum CodingKeys: String, CodingKey {
        case mimeType = "mime_type"
        case data
    }
}

struct GenerationConfig: Codable {
    let temperature: Double?
    let topP: Double?
    let topK: Int?
    let maxOutputTokens: Int?
    let responseMimeType: String?

    enum CodingKeys: String, CodingKey {
        case temperature
        case topP = "topP"
        case topK = "topK"
        case maxOutputTokens = "maxOutputTokens"
        case responseMimeType = "responseMimeType"
    }
}

struct SafetySetting: Codable {
    let category: String
    let threshold: String
}

Response Model

import Foundation

// MARK: - Response

struct GeminiResponse: Codable {
    let candidates: [Candidate]?
    let usageMetadata: UsageMetadata?
    let modelVersion: String?
}

struct Candidate: Codable {
    let content: Content
    let finishReason: String?
    let index: Int?
    let safetyRatings: [SafetyRating]?
}

struct SafetyRating: Codable {
    let category: String
    let probability: String
}

struct UsageMetadata: Codable {
    let promptTokenCount: Int?
    let candidatesTokenCount: Int?
    let totalTokenCount: Int?
}

 

 

정렬하는 방법 시험에 출제

subclass of: 부모 컨트롤러를 작성해야함

 

//
//  GeminiViewController.swift
//  movie_yjy
//
//  Created by comsoft on 2026/06/02.
//

import UIKit // iOS UI 관련 기능 가져오기
// 요청(Request)에 사용할 구조체들
// API에 보낼 JSON 구조를 Swift 구조체로 표현
// Encodable: Swift 객체 → JSON 변환 가능하게 해줌
// 최상위 요청 구조: { "contents": [...] }
struct GeminiRequest: Encodable {
    let contents: [RequestContent] // contents 배열
}
// contents 배열의 각 항목: { "parts": [...] }
struct RequestContent: Encodable {
    let parts: [RequestPart] // parts 배열
}
// parts 배열의 각 항목: { "text": "..." }
struct RequestPart: Encodable {
    let text: String // 실제 프롬프트 텍스트
}
// API가 보내주는 JSON 구조를 Swift 구조체로 표현
// Decodable: JSON → Swift 객체 변환 가능하게 해줌
// 최상위 응답 구조: { "candidates": [...] }
struct GeminiResponse: Decodable {
    let candidates: [Candidate] // candidates 배열
}
// candidates 배열의 각 항목: { "content": {...} }
struct Candidate: Decodable {
    let content: ResponseContent // content 객체
}
// content 객체: { "parts": [...] }
struct ResponseContent: Decodable {
    let parts: [ResponsePart] // parts 배열
}
// parts 배열의 각 항목: { "text": "..." }
struct ResponsePart: Decodable {
    let text: String // AI가 생성한 실제 텍스트
}

class GeminiViewController: UIViewController {
    
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var textView: UITextView!
    
    // Gemini API 키 (Google AI Studio에서 발급)
    let apiKey = "api-key" //자신의 키 입력
    
    @IBAction func buttonTapped(_ sender: UIButton) {
        // 입력값 비어있으면 종료
        guard let mood = textField.text, !mood.isEmpty else { return }
        // 로딩 메시지 표시
        textView.text = "추천 중…"
        // 키보드 내리기
        view.endEditing(true)
        // 영화 추천 받아오기 시작
        getData(mood: mood)
    }
    
    // Gemini API를 호출해서 영화 추천을 받아오는 함수
    func getData(mood: String) {
        let geminiURL =
        "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=\(apiKey)"
        // 문자열 주소를 URL 객체로 변환 (실패하면 함수 종료)
        guard let url = URL(string: geminiURL) else { return }
        // AI에게 보낼 명령(프롬프트) 작성
        // \(mood)에는 사용자가 입력한 기분이 들어감
        let prompt = "기분이 '\(mood)'일 때 볼만한 영화 3편을 한국어로 추천해줘. 각 영화마다 제목, 장르, 추천이유를 간단히 적어줘."
        // 보낼 요청 본문을 구조체로 만들기
        // GeminiRequest > RequestContent > RequestPart 순서로 중첩
        let requestBody = GeminiRequest( contents:
                                            [RequestContent(parts: [RequestPart(text: prompt)])] )
        // HTTP 요청 객체 만들기 (수정 가능하도록 var)
        var request = URLRequest(url: url)
        // 요청 방식을 POST로 설정 (데이터를 "보내는" 방식), GET이 기본
        request.httpMethod = "POST"
        // 헤더에 "JSON 데이터를 보낸다"고 알려줌
        request.setValue("application/json", forHTTPHeaderField:
                            "Content-Type")
        // 구조체를 JSON Data로 바꿔서 요청 본문에 담기
        // try?: 실패하면 nil (간단히 처리)
        request.httpBody = try? JSONEncoder().encode(requestBody)
        // 통신을 담당할 URLSession 객체 생성 (기본 설정 사용)
        let session = URLSession(configuration: .default)
        
        // 통신 작업(task)을 만들기
        // 응답이 도착하면 { } 안의 코드가 실행됨
        // [self]: 클로저 안에서 self를 사용할 수 있게 캡처
        let task = session.dataTask(with: request) { [self]
            (data, response, error) in
            // 에러가 있으면 콘솔에 출력하고 함수 종료
            if error != nil {
                print(error!)
                return
            }
            // 받은 데이터가 없으면 함수 종료
            guard let JSONdata = data else { return }
            
            if let httpResponse = response as? HTTPURLResponse {
                print("statusCode =", httpResponse.statusCode)
            }

            if let jsonString = String(data: JSONdata, encoding: .utf8) {
                print("response =", jsonString)
            }
            // JSON을 Swift 객체로 변환할 디코더 객체 생성
            let decoder = JSONDecoder()
            // 디코딩은 실패할 수 있으므로 do-catch로 감싸기
            do {
                // 받은 JSONdata를 GeminiResponse 구조체로 변환 시도
                let decodedData = try decoder.decode(GeminiResponse.self,
                                                     from: JSONdata)
                // 변환된 구조체에서 실제 추천 텍스트 꺼내기
                // candidates배열의 첫번째 > content > parts배열의 첫번째 > text
                // 만약 중간에 비어있으면 "결과 없음" 사용
                let text =
                decodedData.candidates.first?.content.parts.first?.text ?? "결과 없음"
                
                // 마크다운 기호 제거해서 깔끔하게 만들기
                let cleanText = text
                .replacingOccurrences(of: "**", with: "") // 굵게 표시 ** 제거
                .replacingOccurrences(of: "*", with: "") // 기울임 * 제거
                .replacingOccurrences(of: "##", with: "") // 헤더 ## 제거
                .replacingOccurrences(of: "#", with: "") // 헤더 # 제거
                .replacingOccurrences(of: "`", with: "") // 코드 ` 제거
                // UI 업데이트는 반드시 메인 스레드에서!
                // (네트워크 응답은 백그라운드 스레드에서 도착하기 때문)
                DispatchQueue.main.async {
                    // 정리된 텍스트를 결과창에 표시
                    self.textView.text = cleanText
                }
            } catch {
                // 디코딩 실패하면 에러를 콘솔에 출력
                print(error)
            }
        }
        // resume()을 호출해야 실제로 통신이 시작됨
        task.resume()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Do any additional setup after loading the view.
    }
    
}

 

'iOS' 카테고리의 다른 글

iOS_3grade - 11주차  (0) 2026.05.19
iOS_3grade - 10주차  (1) 2026.05.12
iOS_grade3 - 9주차  (0) 2026.04.28
iOS_3grade - 7주차  (0) 2026.04.14
iOS_3grade - 6주차  (1) 2026.04.07