iOS Class 10

{
  "boxOfficeResult": {
    "boxofficeType": "일별 박스오피스",
    "showRange": "20120101~20120101",
    "dailyBoxOfficeList": [
      {
        "rnum": "1",
        "rank": "1",
        "rankInten": "0",
        "rankOldAndNew": "OLD",
        "movieCd": "20112207",
        "movieNm": "미션임파서블:고스트프로토콜",
        "openDt": "2011-12-15",
        "salesAmt": "2776060500",
        "salesShare": "36.3",
        "salesInten": "-415699000",
        "salesChange": "-13",
        "salesAcc": "40541108500",
        "audiCnt": "353274",
        "audiInten": "-60106",
        "audiChange": "-14.5",
        "audiAcc": "5328435",
        "scrnCnt": "697",
        "showCnt": "3223"
      },
      {
        "rnum": "2",
        "rank": "2",
        "rankInten": "1",
        "rankOldAndNew": "OLD",
        "movieCd": "20110295",
        "movieNm": "마이 웨이",
        "openDt": "2011-12-21",
        "salesAmt": "1189058500",
        "salesShare": "15.6",
        "salesInten": "-105894500",
        "salesChange": "-8.2",
        "salesAcc": "13002897500",
        "audiCnt": "153501",
        "audiInten": "-16465",
        "audiChange": "-9.7",
        "audiAcc": "1739543",
        "scrnCnt": "588",
        "showCnt": "2321"
      },
      {
        "rnum": "3",
        "rank": "3",
        "rankInten": "-1",
        "rankOldAndNew": "OLD",
        "movieCd": "20112621",
        "movieNm": "셜록홈즈 : 그림자 게임",
        "openDt": "2011-12-21",
        "salesAmt": "1176022500",
        "salesShare": "15.4",
        "salesInten": "-210328500",
        "salesChange": "-15.2",
        "salesAcc": "10678327500",
        "audiCnt": "153004",
        "audiInten": "-31283",
        "audiChange": "-17",
        "audiAcc": "1442861",
        "scrnCnt": "360",
        "showCnt": "1832"
      },
      {
        "rnum": "4",
        "rank": "4",
        "rankInten": "0",
        "rankOldAndNew": "OLD",
        "movieCd": "20113260",
        "movieNm": "퍼펙트 게임",
        "openDt": "2011-12-21",
        "salesAmt": "644532000",
        "salesShare": "8.4",
        "salesInten": "-75116500",
        "salesChange": "-10.4",
        "salesAcc": "6640940000",
        "audiCnt": "83644",
        "audiInten": "-12225",
        "audiChange": "-12.8",
        "audiAcc": "895416",
        "scrnCnt": "396",
        "showCnt": "1364"
      },
      {
        "rnum": "5",
        "rank": "5",
        "rankInten": "0",
        "rankOldAndNew": "OLD",
        "movieCd": "20113271",
        "movieNm": "프렌즈: 몬스터섬의비밀 ",
        "openDt": "2011-12-29",
        "salesAmt": "436753500",
        "salesShare": "5.7",
        "salesInten": "-89051000",
        "salesChange": "-16.9",
        "salesAcc": "1523037000",
        "audiCnt": "55092",
        "audiInten": "-15568",
        "audiChange": "-22",
        "audiAcc": "202909",
        "scrnCnt": "290",
        "showCnt": "838"
      },
      {
        "rnum": "6",
        "rank": "6",
        "rankInten": "1",
        "rankOldAndNew": "OLD",
        "movieCd": "19940256",
        "movieNm": "라이온 킹",
        "openDt": "1994-07-02",
        "salesAmt": "507115500",
        "salesShare": "6.6",
        "salesInten": "-114593500",
        "salesChange": "-18.4",
        "salesAcc": "1841625000",
        "audiCnt": "45750",
        "audiInten": "-11699",
        "audiChange": "-20.4",
        "audiAcc": "171285",
        "scrnCnt": "244",
        "showCnt": "895"
      },
      {
        "rnum": "7",
        "rank": "7",
        "rankInten": "-1",
        "rankOldAndNew": "OLD",
        "movieCd": "20113381",
        "movieNm": "오싹한 연애",
        "openDt": "2011-12-01",
        "salesAmt": "344871000",
        "salesShare": "4.5",
        "salesInten": "-107005500",
        "salesChange": "-23.7",
        "salesAcc": "20634684500",
        "audiCnt": "45062",
        "audiInten": "-15926",
        "audiChange": "-26.1",
        "audiAcc": "2823060",
        "scrnCnt": "243",
        "showCnt": "839"
      },
      {
        "rnum": "8",
        "rank": "8",
        "rankInten": "0",
        "rankOldAndNew": "OLD",
        "movieCd": "20112709",
        "movieNm": "극장판 포켓몬스터 베스트 위시「비크티니와 백의 영웅 레시라무」",
        "openDt": "2011-12-22",
        "salesAmt": "167809500",
        "salesShare": "2.2",
        "salesInten": "-45900500",
        "salesChange": "-21.5",
        "salesAcc": "1897120000",
        "audiCnt": "24202",
        "audiInten": "-7756",
        "audiChange": "-24.3",
        "audiAcc": "285959",
        "scrnCnt": "186",
        "showCnt": "348"
      },
      {
        "rnum": "9",
        "rank": "9",
        "rankInten": "0",
        "rankOldAndNew": "OLD",
        "movieCd": "20113311",
        "movieNm": "앨빈과 슈퍼밴드3",
        "openDt": "2011-12-15",
        "salesAmt": "137030000",
        "salesShare": "1.8",
        "salesInten": "-35408000",
        "salesChange": "-20.5",
        "salesAcc": "3416675000",
        "audiCnt": "19729",
        "audiInten": "-6461",
        "audiChange": "-24.7",
        "audiAcc": "516289",
        "scrnCnt": "169",
        "showCnt": "359"
      },
      {
        "rnum": "10",
        "rank": "10",
        "rankInten": "0",
        "rankOldAndNew": "OLD",
        "movieCd": "20112708",
        "movieNm": "극장판 포켓몬스터 베스트 위시 「비크티니와 흑의 영웅 제크로무」",
        "openDt": "2011-12-22",
        "salesAmt": "125535500",
        "salesShare": "1.6",
        "salesInten": "-40756000",
        "salesChange": "-24.5",
        "salesAcc": "1595695000",
        "audiCnt": "17817",
        "audiInten": "-6554",
        "audiChange": "-26.9",
        "audiAcc": "235070",
        "scrnCnt": "175",
        "showCnt": "291"
      }
    ]
  }
}
//
//  ViewController.swift
//  MovieYYC
//
//  Created by 소프트웨어컴퓨터 on 2026/05/04.
//

import UIKit
let movie = ["슈퍼 마리오 갤럭시", "악마는 프라다를 입는다 2", "살목지", "프로젝트 헤일메리", "짱구", "왕과 사는 남자", "란 12.3", "내 이름은", "사랑의 하츄핑 특별판", "극장판 반짝반짝 달님이: 싱어롱 파티"]
let accumulatedAudience: [String] = [
    "685993",
    "736041",
    "2636101",
    "2649590",
    "298506",
    "16779516",
    "199098",
    "191975",
    "7546",
    "5950"
]
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
            
//        let rank = indexPath.row + 1
//        let name = movie[indexPath.row]
//        let audience = accumulatedAudience[indexPath.row]
//        let formatter = NumberFormatter()
//        formatter.numberStyle = .decimal
//
//        if let number = Int(audience),
//           let formatted = formatter.string(from: NSNumber(value: number)) {
//            cell.movieName.text = "[\(rank)등] \(name): \(formatted)명"
//        }
        cell.movieName.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    @IBOutlet weak var table: UITableView!
    var movieData : MovieData?
    var movieURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=&targetDt="
    
    struct MovieData : Codable {
    let boxOfficeResult : BoxOfficeResult
    }
    struct BoxOfficeResult : Codable {
    let dailyBoxOfficeList : [DailyBoxOfficeList]
    }
    struct DailyBoxOfficeList : Codable {
    let movieNm : String
    let audiCnt : String
    let audiAcc : String
    let rank : String
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        table.delegate = self
        table.dataSource = self
        
        movieURL += makeYesterdayString()
        getData()
    }
    func makeYesterdayString() -> String{
        let y = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
        let dateF = DateFormatter()
        dateF.dateFormat = "yyyyMMdd"
        let day = dateF.string(from: y)
        return day
    }
    func getData(){
        guard let url = URL(string: movieURL) else {return}
        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: url) { data, response, error in
            if error != nil {
                print(error!)
                return
            }
            guard let JSONdata = data else {return}
            let dataString = String(data: JSONdata, encoding: .utf8)
            print(dataString!)
            let decoder = JSONDecoder()
            do {
                let decodedData = try decoder.decode(MovieData.self, from: JSONdata)
                print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm)
                self.movieData = decodedData
                DispatchQueue.main.async {
                    self.table.reloadData()
                }
                
            }catch{
                print(error)
            }
        }
        task.resume()
    }

}

// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
//   let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)

import Foundation

// MARK: - Welcome
struct Welcome {
    let boxOfficeResult: BoxOfficeResult
}

// MARK: - BoxOfficeResult
struct BoxOfficeResult {
    let boxofficeType, showRange: String
    let dailyBoxOfficeList: [DailyBoxOfficeList]
}

// MARK: - DailyBoxOfficeList
struct DailyBoxOfficeList {
    let rnum, rank, rankInten: String
    let rankOldAndNew: RankOldAndNew
    let movieCD, movieNm, openDt, salesAmt: String
    let salesShare, salesInten, salesChange, salesAcc: String
    let audiCnt, audiInten, audiChange, audiAcc: String
    let scrnCnt, showCnt: String
}

enum RankOldAndNew: String {
    case old
}
import UIKit

// MARK: - 데이터 모델 (Codable: JSON ↔ Swift 구조체 자동 변환)

// JSON 최상위 구조에 대응하는 구조체
struct MovieData: Codable {
    let boxOfficeResult: BoxOfficeResult  // JSON 키 "boxOfficeResult"와 이름 일치 필수
}

// 박스오피스 결과 구조체
struct BoxOfficeResult: Codable {
    let dailyBoxOfficeList: [DailyBoxOfficeList]  // 영화 목록 배열
}

// 개별 영화 정보 구조체 (JSON에서 필요한 필드만 선택적으로 선언 가능)
struct DailyBoxOfficeList: Codable {
    let movieNm: String  // 영화명
    let audiCnt: String  // 당일 관객수
    let audiAcc: String  // 누적 관객수
    let rank: String     // 순위
}

// MARK: - ViewController
// UITableViewDelegate: 테이블뷰 동작(선택, 높이 등) 처리
// UITableViewDataSource: 테이블뷰에 표시할 데이터 제공
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    // 스토리보드에서 연결한 테이블뷰
    @IBOutlet weak var table: UITableView!
    
    // 네트워크에서 받아온 영화 데이터 저장용 프로퍼티
    // Optional: 데이터를 받아오기 전까지는 nil 상태
    var movieData: MovieData?
    
    // 영진위 박스오피스 API URL (뒤에 날짜 문자열을 붙여서 완성)
    var movieURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=...&targetDt="
    
    
    // MARK: - UITableViewDataSource 필수 메서드 ①
    // 테이블뷰의 행(row) 개수 반환
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10  // 박스오피스 순위는 항상 10개
    }
    
    // MARK: - UITableViewDataSource 필수 메서드 ②
    // 각 행에 표시할 셀을 구성해서 반환
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        // dequeueReusableCell: 재사용 큐에서 셀을 꺼냄
        // 스크롤 시 화면 밖으로 나간 셀을 새로 만들지 않고 재사용 → 메모리 절약
        // as! MyTableViewCell: 커스텀 셀 클래스로 다운캐스팅
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
        
        // indexPath.row: 현재 행 번호 (0부터 시작)
        // Optional chaining(?.)으로 movieData가 nil이면 nil 반환 → 앱 크래시 방지
        cell.movieName.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm
        
        return cell
    }
    
    // 섹션 개수 반환 (박스오피스는 1개 섹션으로 충분)
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    // MARK: - 뷰 생명주기
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 테이블뷰의 delegate, dataSource를 이 ViewController가 담당하도록 연결
        table.delegate = self
        table.dataSource = self
        
        // URL 완성: 기본 URL + 어제 날짜 문자열 (예: "20260510")
        movieURL += makeYesterdayString()
        
        // 네트워크 요청 시작
        getData()
    }

    // MARK: - 어제 날짜를 yyyyMMdd 형식 문자열로 반환
    func makeYesterdayString() -> String {
        // Calendar.current: 현재 기기의 캘린더
        // byAdding: .day, value: -1 → 오늘에서 1일 빼기
        // Date(): 현재 날짜/시간
        let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
        
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyyMMdd"  // yyyy: 연도, MM: 월, dd: 일
        
        return formatter.string(from: yesterday)  // Date → String 변환
    }
    
    // MARK: - 네트워크 데이터 요청
    func getData() {
        // 1단계: String → URL 타입 변환 (failable initializer이므로 guard let 사용)
        guard let url = URL(string: movieURL) else { return }
        
        // 2단계: URLSession 생성 (.default: 기본 설정의 세션)
        let session = URLSession(configuration: .default)
        
        // 3단계: dataTask 생성 - url로 GET 요청을 보내고 응답을 클로저에서 처리
        // 클로저는 백그라운드 스레드에서 실행됨
        let task = session.dataTask(with: url) { data, response, error in
            
            // 네트워크 에러 체크 (인터넷 끊김, 타임아웃 등)
            if error != nil {
                print(error!)
                return  // 에러 발생 시 이후 코드 실행 중단
            }
            
            // data가 nil이면 종료, 있으면 JSONdata에 바인딩
            guard let JSONdata = data else { return }
            
            // Data → String 변환 (JSON 원문 확인용, 디버깅 시 주석 해제)
            let dataString = String(data: JSONdata, encoding: .utf8)
            // print(dataString!)
            
            // JSONDecoder: JSON(Data 타입) → Swift 구조체로 변환
            let decoder = JSONDecoder()
            
            do {
                // try: decode()는 실패할 수 있는 함수(throws)이므로 try 필요
                // MovieData.self: 어떤 구조체로 변환할지 타입 전달
                let decodedData = try decoder.decode(MovieData.self, from: JSONdata)
                
                // 디코딩 확인용 출력 (테스트 후 제거 가능)
                print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm)
                print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audiAcc)
                print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audiCnt)
                
                // 받아온 데이터를 프로퍼티에 저장
                self.movieData = decodedData
                
                // UI 업데이트는 반드시 메인 스레드에서 실행해야 함
                // dataTask 클로저는 백그라운드 스레드이므로 DispatchQueue.main.async로 전환
                DispatchQueue.main.async {
                    self.table.reloadData()  // 테이블뷰 전체 새로고침
                }
            } catch {
                // 디코딩 실패 원인: JSON 키와 구조체 프로퍼티 이름 불일치,
                // 타입 불일치, 필수 키 누락 등
                print(error)
            }
        }
        
        // 4단계: task 시작 (resume() 호출 전까지 task는 일시정지 상태)
        task.resume()
    }
    
}