[새싹 iOS] 4주차_코드

임승섭·2023년 8월 13일
0

새싹 iOS

목록 보기
11/45

Kakao Book Search

//
//  KakaoBookViewController.swift
//  0809hw
//
//  Created by 임승섭 on 2023/08/09.
//

import UIKit
import SwiftyJSON
import Alamofire
import Kingfisher

/* ===== 8/9 과제 =====*/
// 8/9 과제
// 1. 카카오 책 검색 테이블뷰
// 2. searchBar로 검색 눌러야 테이블뷰 업데이트
// 3. Pagenation -> prefetching
// 4. 쿼리 활용
    // 4 - 1. 검색 정렬 (정확도, 최신순) -> tableView header에 view 넣고 그 안에 pull down button
    // 4 - 2. 검색 제한 (전체, title, publisher, person) -> searchBar 옆에 pull down button


// 필요한 구조체. 레이블에 띄워줄 문구는 연산 프로퍼티로 받아오기
struct Book {
    let authors: String
    let datetime: String
    let price: Int
    let publisher: String
    let salePrice: Int
    let thumbnail: String
    let title: String
    
    var content1: String {  // 제목
        return "\(title)"
    }
    var content2: String {  // 작가 / 출판사
        return "\(authors) | \(publisher)"
    }
    var content3: String {  // 출판일
        return datetime
    }
    var content4: String {  // 할인율 (오랜만에 원가, 정가, 판매금액 계산하네..)
        // 판매가가 -1 이면 절판
        if (salePrice == -1 ) {
            return "절판"
        }
        else {
            let dcPercent = lround( ( (Double(price) - Double(salePrice) ) / Double(price) ) * 100)
            return "\(price) -> \(salePrice) ( \(dcPercent) % 할인 )"
        }
    }
}

// 오른쪽 pull down button에 들어갈 항목. 레이블로 띄워줄 문구는 연산 프로퍼티
enum sorting: String {
    case accuracy
    case latest
    
    var forMenu: String {
        switch self {
        case .accuracy : return "정확도순"
        case .latest : return "발간일순"
        }
    }
}

// 왼쪽 pull down button에 들어갈 항목. 레이블로 띄워줄 문구는 연산 프로퍼티
enum targeting: String {
    case all
    case title
    case publisher
    case person
    
    var forMenu: String {
        switch self {
        case .all : return "전체"
        case .title : return "제목"
        case .publisher : return "출판사"
        case .person : return "인명"
        }
    }
}




class KakaoBookViewController: UIViewController {

    @IBOutlet var searchBar: UISearchBar!
    @IBOutlet var bookTableView: UITableView!
    
    @IBOutlet var targetPullDownButton: UIButton!
    @IBOutlet var sortPullDownButton: UIButton!
    
    
    var bookList: [Book] = []   // 테이블뷰에 보여줄 Book 목록
    var pageCnt = 1             // 페이지 수. 스크롤할수록 증가 (pagenation)
    var isEnd = false           // 더이상 보여줄 항목이 없을 때
    
    var sortingOption = sorting.accuracy
    var targetingOption = targeting.all
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        searchBar.delegate = self
        bookTableView.dataSource = self
        bookTableView.delegate = self
        bookTableView.prefetchDataSource = self
        
        bookTableView.rowHeight = 120
        
        designSorting(sortPullDownButton)
        designTargeting(targetPullDownButton)
    }
    
    
    
    func callRequest(_ query: String, _ pageCnt: Int) {
        
        // 1. 검색할 문자열
        guard let txt = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return }
        
        // 2 - 1. target option (all이면 따로 쿼리 필요 없지만, all 아니면 쿼리 추가)
        var target = ""
        if (targetingOption != .all) {
            target = "&target=\(targetingOption.rawValue)"
        }
        
        // 2 - 2. sort option
        let sort = "&sort=\(sortingOption.rawValue)"
        
        // 3. 최종 url
        let url = "https://dapi.kakao.com/v3/search/book?query=\(txt)&size=10&page=\(pageCnt)\(target)\(sort)"
        
        // 4. header (HTTPHeader 아님!!)
        let header: HTTPHeaders = ["Authorization" : APIKey.kakao]
        
        // SwiftyJSON : Work with Alamofire
        AF.request(url, method: .get, headers: header)
            .validate(statusCode: 200...500)    // 구체적인 에러 내용을 알기 위해 success 범위 조정
            .responseJSON { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                let statusCode = response.response?.statusCode ?? 500
                
                // 성공(200)인 경우 작업 시작
                if (statusCode == 200) {
                    self.isEnd = json["meta"]["is_end"].boolValue
                    
                    for book in json["documents"].arrayValue {
                        let authors = book["authors"][0].stringValue
                        let datetime = book["datetime"].stringValue
                        let price = book["price"].intValue
                        let publisher = book["publisher"].stringValue
                        let salePrice = book["sale_price"].intValue
                        let thumbnail = book["thumbnail"].stringValue
                        let title = book["title"].stringValue
                        
                        let newBook = Book(authors: authors, datetime: datetime, price: price, publisher: publisher, salePrice: salePrice, thumbnail: thumbnail, title: title)
                        
                        self.bookList.append(newBook)
                    }
                    
                    self.bookTableView.reloadData();
                }
                // 성공 못하면 메세지 출력
                else {
                    print("에러났어용")
                    print(json)
                }
            case .failure(let error):
                print(error)
            }
        }
    }
    
    // pull down button 디자인
    // 1. 현재 선택된 옵션으로 버튼 타이틀 설정
    // 2. 특정 옵션을 사용자가 선택하면
        // 2 - 1. Option 변수 수정
        // 2 - 2. 타이틀 수정
        // 2 - 3. pageCnt 초기화 (1)
        // 2 - 4. 기존 배열(bookList) 초기화
        // 2 - 5. callRequest 호출
    
    func designSorting(_ sender: UIButton) {
        sender.setTitle(sortingOption.forMenu, for: .normal)
        
        let op1 = UIAction(title: sorting.accuracy.forMenu) { _ in
            self.sortingOption = sorting.accuracy
            sender.setTitle(self.sortingOption.forMenu, for: .normal)
            self.pageCnt = 1
            self.bookList.removeAll()
            self.callRequest(self.searchBar.text!, self.pageCnt)
        }
        let op2 = UIAction(title: sorting.latest.forMenu) { _ in
            self.sortingOption = sorting.latest
            sender.setTitle(self.sortingOption.forMenu, for: .normal)
            self.pageCnt = 1
            self.bookList.removeAll()
            self.callRequest(self.searchBar.text!, self.pageCnt)
        }
        
        let buttonMenu = UIMenu(title: "정렬 기준을 고르세요", children: [op1, op2])
        sender.menu = buttonMenu
    }
    func designTargeting(_ sender: UIButton) {
        sender.setTitle(targetingOption.forMenu, for: .normal)
        
        let op1 = UIAction(title: targeting.all.forMenu) { _ in
            self.targetingOption = targeting.all
            sender.setTitle(self.targetingOption.forMenu, for: .normal)
            self.pageCnt = 1
            self.bookList.removeAll()
            self.callRequest(self.searchBar.text!, self.pageCnt)
        }
        let op2 = UIAction(title: targeting.title.forMenu) { _ in
            self.targetingOption = targeting.title
            sender.setTitle(self.targetingOption.forMenu, for: .normal)
            self.pageCnt = 1
            self.bookList.removeAll()
            self.callRequest(self.searchBar.text!, self.pageCnt)
        }
        let op3 = UIAction(title: targeting.publisher.forMenu) { _ in
            self.targetingOption = targeting.publisher
            sender.setTitle(self.targetingOption.forMenu, for: .normal)
            self.pageCnt = 1
            self.bookList.removeAll()
            self.callRequest(self.searchBar.text!, self.pageCnt)
        }
        let op4 = UIAction(title: targeting.person.forMenu) { _ in
            self.targetingOption = targeting.person
            sender.setTitle(self.targetingOption.forMenu, for: .normal)
            self.pageCnt = 1
            self.bookList.removeAll()
            self.callRequest(self.searchBar.text!, self.pageCnt)
        }
        
        let buttonMenu = UIMenu(title: "검색 필드를 고르세요", children: [op1, op2, op3, op4])
        sender.menu = buttonMenu
    }
}


extension KakaoBookViewController: UITableViewDelegate, UITableViewDataSource, UITableViewDataSourcePrefetching {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return bookList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "KakaoBookTableViewCell") as! KakaoBookTableViewCell
        
        // cell 내용
        if let url = URL(string: bookList[indexPath.row].thumbnail) {
            cell.mainImageView.kf.setImage(with: url)
        }
        cell.firstLabel.text = bookList[indexPath.row].content1
        cell.secondLabel.text = bookList[indexPath.row].content2
        cell.thirdLabel.text = bookList[indexPath.row].content3
        cell.fourthLabel.text = bookList[indexPath.row].content4
        
        return cell
    }
    
    // 셀이 화면에 등장하기 전에 실행. 미리 pageCnt를 올릴지 결정
    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        print("===== 프리패치 \(indexPaths) =====")
        
        for indexPath in indexPaths {
            if (bookList.count-1 == indexPath.row &&
                pageCnt < 15 &&
                !isEnd ) {
                pageCnt += 1
                callRequest(searchBar.text!, pageCnt)
            }
        }
    }
    
    // 빠르게 넘기는 셀들은 데이터 필요 없기 때문에 취소
    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
        print("===== 취소여 \(indexPaths) =====")
    }
}

extension KakaoBookViewController: UISearchBarDelegate {
    // 검색 실행
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        pageCnt = 1
        bookList.removeAll()
        
        guard let query = searchBar.text else {return }
        
        // 빈 문자열이면 그냥 테이블뷰만 리로드
        if query == "" {
            bookTableView.reloadData()
        }
        else {
            callRequest(searchBar.text!, pageCnt)
        }
    }
}

Papago Translator

//
//  Translate2ViewController.swift
//  0810hw
//
//  Created by 임승섭 on 2023/08/10.
//

import UIKit
import SwiftyJSON
import Alamofire


class Translate2ViewController: UIViewController {
    
    
    @IBOutlet var firstTextField: UITextField!
    @IBOutlet var secondTextField: UITextField!
    @IBOutlet var firstTextView: UITextView!
    @IBOutlet var secondTextView: UITextView!
    @IBOutlet var translateButton: UIButton!
    
    let firstPickerView = UIPickerView()
    let secondPickerView = UIPickerView()
    
    // 레이블에 띄워줄 문구: api 쿼리
    let dict = [
        "Korean": "ko",
        "English": "en",
        "Japanese": "ja",
        "Chinese - CN": "zh-CN",
        "Chinese - TW": "zh-TW",
        "Vietnam": "vi",
        "Indonesia": "id",
        "Thailand": "th",
        "German": "de",
        "Russian": "ru",
        "Spanish": "es",
        "Italy": "it",
        "Franch": "fr"
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        design(firstTextField)
        design(secondTextField)
        design(firstTextView)
        design(secondTextView)
        design(translateButton)
        
        translateButton.setTitle("Translate!", for: .normal)
        
        firstPickerView.delegate = self
        firstPickerView.dataSource = self
        secondPickerView.delegate = self
        secondPickerView.dataSource = self
        
        firstTextField.inputView = firstPickerView
        secondTextField.inputView = secondPickerView
        
        firstTextField.placeholder = "언어를 선택하세요"
        secondTextField.placeholder = "언어를 선택하세요"
        
        firstTextView.text = ""
        secondTextView.text = ""
        secondTextView.isEditable = false
    }
    
    func design(_ sender: UIView) {
        sender.layer.borderWidth = 1
    }
    
    
    
    @IBAction func tranlateButtonTapped(_ sender: UIButton) {
        if ( firstTextField.text == "" || secondTextField.text == "") {
            return;
        }
        
        
        // 최종 url
        let url = "https://openapi.naver.com/v1/papago/n2mt"
        
        // header (HTTPHeader 아님!!)
        let header: HTTPHeaders = [
            "X-Naver-Client-Id" : APIKey.naver,
            "X-Naver-Client-Secret" : APIKey.naverSecret
        ]
        
        let parameter: Parameters = [
            "source": dict[firstTextField.text!]!,
            "target": dict[secondTextField.text!]!,
            "text": firstTextView.text!
        ]
        
        // SwiftyJSON : Work with Alamofire
        AF.request(url, method: .post, parameters: parameter, headers: header)
            .validate()
            .responseJSON { response in
                switch response.result {
                case .success(let value):
                    let json = JSON(value)
                    print("JSON: \(json)")
                    
                    self.secondTextView.text = json["message"]["result"]["translatedText"].stringValue
                case .failure(let error):
                    print(error)
                }
            }

    }
    
    
    @IBAction func tapgesture(_ sender: UITapGestureRecognizer) {
        view.endEditing(true)
    }
}


extension Translate2ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        dict.keys.count
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        if (pickerView == firstPickerView) {
            firstTextField.text = String( Array(dict.keys)[row] )
        } else {
            secondTextField.text = String( Array(dict.keys)[row] )
        }
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return String( Array(dict.keys)[row] )
    }
}

TMDB Movie Search

URL + Extension

//
//  URL+Extension.swift
//  0811hw
//
//  Created by 임승섭 on 2023/08/11.
//

import Foundation

extension URL {
    
    static let baseURL = "https://api.themoviedb.org/3/"
    
    static func makeEndPointString(_ endpoint: String) -> String {
        return baseURL + endpoint
    }
}

enum Endpoint {
    case movieTrend
    case movieGenre
    case movieDetail
    case imagePrefix
    
    var requestURL: String {
        switch self {
        case .movieTrend :  return URL.makeEndPointString("trending/movie/week?language=en-US")
        case .movieGenre :  return URL.makeEndPointString("genre/movie/list")
        case .movieDetail : return URL.makeEndPointString("movie/") // 뒤에 추가해줘야 함 : "\(movieID)/credits"
        case .imagePrefix : return "https://image.tmdb.org/t/p/w500/"
        }
    }
}

APIManager

//
//  APIManager.swift
//  0811hw
//
//  Created by 임승섭 on 2023/08/11.
//

import Foundation
import SwiftyJSON
import Alamofire

class APIManager {
    
    static let shared = APIManager()
    private init() {}
    
    let header: HTTPHeaders = ["Authorization" : APIKey.tmdb]
    
    func callRequest(_ type: Endpoint, _ movieID: Int, completionHandler: @escaping (JSON) -> () )  {
        
        var url = type.requestURL
        if (type == .movieDetail) {
            url += "\(movieID)/credits"
        }
        
        AF.request(url, method: .get, headers: header)
            .validate()
            .responseJSON { response in
                switch response.result {
                case .success(let value) :
                    let json = JSON(value)
                    completionHandler(json)
                    
                case .failure(let error) :
                    print(error)
            }
            
        }
    }
}

MainViewController

//
//  MainViewController.swift
//  0811hw
//
//  Created by 임승섭 on 2023/08/11.
//

import UIKit
import SwiftyJSON
import Alamofire

class MainViewController: UIViewController {
    
    var movieList: [MovieForMain] = []
    
    
    @IBOutlet var mainTableView: UITableView!
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        
        mainTableView.delegate = self
        mainTableView.dataSource = self
        
        let nib = UINib(nibName: "MovieTableViewCell", bundle: nil)
        mainTableView.register(nib, forCellReuseIdentifier: "MovieTableViewCell")
        
        mainTableView.rowHeight = 400

        // callRequest가 끝나고 callRequest2가 실행되도록 @escaping 사용
        callRequest(callRequest2)
    }
    
    // 장르 api 호출
    func callRequest2() {
        
        APIManager.shared.callRequest(.movieGenre, 0) { json in
            // 3중 loop...?
            for (index, movie) in self.movieList.enumerated() {
                print(index, movie.genre)
                self.movieList[index].genreString.removeAll()
                
                for movieGenre in movie.genre {
                    
                    for g in json["genres"].arrayValue {
                        
                        if g["id"].intValue == movieGenre {
                            self.movieList[index].genreString.append(g["name"].stringValue)
                        }
                        print(index, self.movieList[index].genreString)
                    }
                }
            }
            
            self.mainTableView.reloadData()
        }
    }
    
    // 영화 api 호출
    func callRequest(_  completion: @escaping () -> Void) {
        
        APIManager.shared.callRequest(.movieTrend, 0) { json in
            for item in json["results"].arrayValue {
                print(item)
                
                let date = item["release_date"].stringValue
                
                var genre: [Int] = []
                for g in item["genre_ids"].arrayValue {
                    genre.append(g.intValue)
                }
                
                let mainImage = item["poster_path"].stringValue
                let backImage = item["backdrop_path"].stringValue
                let rate = item["vote_average"].doubleValue
                let title = item["title"].stringValue
                
                let id = item["id"].intValue
                let overView = item["overview"].stringValue
                
                let newMovie = MovieForMain(id: id, date: date, genre: genre, genreString: [], mainImage: mainImage, backImage: backImage, rate: rate, title: title, overview: overView)
                
                self.movieList.append(newMovie)
            }
            
            // 영화에 대한 정보를 받아왔으면, 이제 장르코드에 맞춰서 장르 문자열 배열로 저장
            completion()
            
            self.mainTableView.reloadData()
        }
    }
    

}


extension MainViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return movieList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MovieTableViewCell") as! MovieTableViewCell
        
        
        cell.designCell(movieList[indexPath.row])
        
        return cell;
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vc = storyboard?.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
        
        vc.movieID = movieList[indexPath.row].id
        vc.movieName = movieList[indexPath.row].title
        vc.overView = movieList[indexPath.row].overview
        
        vc.mainImageLink = movieList[indexPath.row].mainImage
        vc.backImageLink = movieList[indexPath.row].backImage
        
        tableView.reloadRows(at: [indexPath], with: .automatic)
        
        navigationController?.pushViewController(vc, animated: true)
    }
}

DetailViewController

//
//  DetailViewController.swift
//  0811hw
//
//  Created by 임승섭 on 2023/08/11.
//

import UIKit
import Alamofire
import SwiftyJSON

class DetailViewController: UIViewController {
    
    
    @IBOutlet var titleLabel: UILabel!
    @IBOutlet var backImageView: UIImageView!
    @IBOutlet var mainImageView: UIImageView!
    
    @IBOutlet var overTextView: UITextView!
    @IBOutlet var castTextView: UITextView!
    @IBOutlet var crewTextView: UITextView!
    
    
    var movieID: Int = 0
    var movieName: String = ""
    var overView: String = ""
    
    var mainImageLink: String = ""
    var backImageLink: String = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        print(movieID)
        
        titleLabel.text = movieName
        overTextView.text = overView
        
        castTextView.text = ""
        crewTextView.text = ""
        
        overTextView.isEditable = false
        castTextView.isEditable = false
        crewTextView.isEditable = false
        
        
        let urlMain = URL(string: Endpoint.imagePrefix.requestURL + mainImageLink)
        mainImageView.kf.setImage(with: urlMain)
        
        let urlBack = URL(string: Endpoint.imagePrefix.requestURL + backImageLink)
        backImageView.kf.setImage(with: urlBack)
        backImageView.contentMode = .scaleAspectFill
        
        // 실질적으로 api에서 받아와서 레이블에 써주는건 cast, crew 리스트
        // 나머지는 화면 전환 시 값 전달로 받아옴
        APIManager.shared.callRequest(.movieDetail, movieID) { json in
            for person in json["cast"].arrayValue {
                self.castTextView.text += person["name"].stringValue
                self.castTextView.text += "\n"
            }
            
            for person in json["crew"].arrayValue {
                self.crewTextView.text += person["name"].stringValue
                self.crewTextView.text += "\n"
            }
        }
    }
}

0개의 댓글