예외처리가 안되어서 생긴 오류입니다.
클로저 안에서 movieData라는 인스턴스 프로퍼티를 사용하려고 할 때, 해당 프로퍼티가 속한 객체를 명시적으로(self.movieData) 써줘야 한다는 의미입니다.
//
// ViewController.swift
// Movieksh
//
// Created by comsoft on 2025/05/08.
//
import UIKit
// 샘플 영화명 배열 (실제 사용 X, 예시로 남아있음)
let name = ["야당", "썬더볼츠", "거룩한 밤 : 데몬즈 헌터", "파과", "바이러스"]
// API에서 받아온 전체 박스오피스 결과 구조체
struct MovieData : Codable {
let boxOfficeResult : BoxOfficeResult
}
// 박스오피스 API의 실제 결과 구조체
struct BoxOfficeResult : Codable {
let dailyBoxOfficeList : [DailyBoxOfficeList] // 일일 박스오피스 리스트 (상위 10개 영화)
}
// 영화 1개에 대한 정보 구조체
struct DailyBoxOfficeList : Codable {
let movieNm : String // 영화 이름
let audiCnt : String // 전일 관객수(숫자 문자열)
let audiAcc : String // 누적 관객수(숫자 문자열)
let rank : String // 순위(숫자 문자열)
}
// 메인 뷰 컨트롤러 클래스
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
@IBOutlet weak var table: UITableView! // TableView 아웃렛
var movieData : MovieData? // API에서 받아온 데이터를 담는 변수
// API 호출 url. (자신의 키값에 API KEY를 반드시 입력해야 함)
var movieURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=(자신의 키값)&targetDt="
// 뷰가 메모리에 올라왔을 때 호출되는 함수
override func viewDidLoad() {
super.viewDidLoad()
table.dataSource = self // 데이터 소스 연결
table.delegate = self // 델리게이트 연결
movieURL += makeYesterdayString() // URL에 어제 날짜 붙이기
getData() // 데이�� 가져오기 시작
}
// 어제 날짜(yyyyMMdd 문자열) 만드는 함수
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
}
// API 데이터를 받아오는 함수
func getData(){
guard let url = URL(string: movieURL) else { return } // URL 생성
let session = URLSession(configuration: .default) // URL세션 생성
let task = session.dataTask(with: url) { data, response, error in
// 네트워크 에러 체크
if error != nil {
print(error!)
return
}
guard let JSONdata = data else { return } // 데이터가 없으면 종료
// 디버깅용(필요시 JSON String 확인)
// let dataString = String(data: JSONdata, encoding: .utf8)
// print(dataString!)
let decoder = JSONDecoder()
do {
// JSON -> 구조체로 디코딩
let decodedData = try decoder.decode(MovieData.self, from: JSONdata)
// 결과 저장
self.movieData = decodedData
// UI 업데이트는 메인 스레드에서undefined
// UI 업데이트(테이블 리로드)는 메인 스레드에서 해야 함
DispatchQueue.main.async {
self.table.reloadData()
}
} catch {
// 디코딩 에러 시 메시지 출력
print(error)
}
}
task.resume() // 네트워크 작업 시작
}
// 테이블 뷰의 셀 개수(행 수) 반환: 영화 10개 (API 기본 10개)
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
cell.movieName.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm
cell.audiAccumulate.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiAcc
cell.audiCount.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiCnt
return cell
}
*/
// 각 셀에 출력할 내용 설정 (커스텀 셀 MyTableViewCell 사용)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 셀 재사용 혹은 새로 생성
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
// rank(순위)와 movieNm(영화명) guard로 안전하게 꺼냄, 실패시 기본 셀 반환
guard let mRank = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].rank else { return UITableViewCell() }
guard let mName = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm else { return UITableViewCell() }
cell.movieName.text = "[\(mRank)위] \(mName)" // 텍스트 포맷팅
// 어제 관객 수 표시 (숫자를 천단위 콤마 처리 후 "명" 붙여 출력)
if let aCnt = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiCnt {
let numF = NumberFormatter()
numF.numberStyle = .decimal // 천단위 콤마
let aCount = Int(aCnt)! // 문자열 -> Int
let result = numF.string(for: aCount)! + "명"
cell.audiCount.text = "어제: \(result)"
}
// 누적 관객수도 똑같이 처리
if let aAcc = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiAcc {
let numF = NumberFormatter()
numF.numberStyle = .decimal
let aAcc1 = Int(aAcc)!
let result = numF.string(for: aAcc1)! + "명"
cell.audiAccumulate.text = "누적: \(result)"
}
return cell // 완성된 셀 반환
}
// 테이블 뷰의 헤더(섹션 제목). 어제 날짜를 포함하여 박스오피스 정보 표시
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?undefined
// 테이블 뷰의 헤더(섹션 제목). 어제 날짜를 포함하여 박스오피스 정보 표시
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// 어제 날짜를 만들어서 헤더 제목에 삽입
return "🍿박스오피스(영화진흥위원회제공:" + makeYesterdayString() + ")🍿"
}
// 테이블 뷰의 푸터(섹션 마지막 라인)에 표시될 텍스트
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return "made by ksh"
}
// 셀을 선택했을 때 호출되는 함수
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 선택된 셀의 indexPath 출력(디버깅 용도)
print(indexPath.description)
}
// 섹션의 개수 반환(여기서는 1개만 표시)
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
리팩터링
//리팩터링
import UIKit
// MARK: - Models
struct MovieData: Codable {
let boxOfficeResult: BoxOfficeResult
}
struct BoxOfficeResult: Codable {
let dailyBoxOfficeList: [DailyBoxOffice]
}
struct DailyBoxOffice: Codable {
let movieNm: String
let audiCnt: String
let audiAcc: String
let rank: String
}
// MARK: - ViewController
class ViewController: UIViewController {
// MARK: - Properties
@IBOutlet weak var tableView: UITableView!
private var movies: [DailyBoxOffice] = []
// API KEY를 꼭 본인의 키로 교체하세요!
private let apiKey = "(여기에_본인의_API_KEY_입력)"
private let serviceURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json"
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
fetchMovieData(for: yesterdayString())
}
// MARK: - TableView Setup
private func configureTableView() {
tableView.dataSource = self
tableView.delegate = self
}
// MARK: - Networking
private func fetchMovieData(for date: String) {
guard let url = makeMovieURL(for: date) else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
if let error = error {
print("Network error:", error)
return
}
guard let data = data else { return }
do {
let decoded = try JSONDecoder().decode(MovieData.self, from: data)
self?.movies = decoded.boxOfficeResult.dailyBoxOfficeList
DispatchQueue.main.async {
self?.tableView.reloadData()
}
} catch {
print("Decoding error:", error)
}
}
task.resume()
}
private func makeMovieURL(for date: String) -> URL? {
var components = URLComponents(string: serviceURL)
components?.queryItems = [
URLQueryItem(name: "key", value: apiKey),
URLQueryItem(name: "targetDt", value: date)
]
return components?.url
}
// MARK: - Date Utility
private func yesterdayString() -> String {
let calendar = Calendar.current
guard let yesterday = calendar.date(byAdding: .day, value: -1, to: Date()) else {
return ""
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd"
return formatter.string(from: yesterday)
}
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int { 1 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movies.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for:undefined
guard
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as? MyTableViewCell,
movies.indices.contains(indexPath.row)
else {
return UITableViewCell()
}
let movie = movies[indexPath.row]
cell.movieName.text = "[\(movie.rank)위] \(movie.movieNm)"
cell.audiCount.text = "어제: \(movie.audiCnt.formattedDecimal)명"
cell.audiAccumulate.text = "누적: \(movie.audiAcc.formattedDecimal)명"
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "🍿박스오피스(영화진흥위원회 제공: \(yesterdayString()))🍿"
}
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return "made by ksh"
}
}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 셀 선택시 로그 출력 등 추가 처리
print("Selected row: \(indexPath.row)")
tableView.deselectRow(at: indexPath, animated: true)
}
}
// MARK: - String Extension (숫자 콤마 포매팅)
private extension String {
var formattedDecimal: String {
guard let intValue = Int(self) else { return self }
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter.string(for: intValue) ?? self
}
}