카테고리 없음

iOS프로그래밍실무 (05.15)

k0223 2025. 5. 15. 16:30

예외처리가 안되어서 생긴 오류입니다.

 

클로저 안에서 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
    }
}