[iOS 사전캠프] Step3.Lv1.2. 성적 관리 시스템 제작하기

DoyleHWorks·2024년 10월 14일
0

문제

내 코드

import Foundation // Foundation 프레임워크 임포트

// A. 학생 정보 저장
var STUDENTS: [String: String] = [:]

// B. 학생별 듣는 과목들 저장
var SUBJECTS: [String: Set<String>] = [:]

// C. 학생별 과목별 성적 저장
var GRADES: [String: [String: [Int]]] = [:] // 학생 번호 -> 과목 -> 성적 배열

// I. 학생 관리

// 1. 학생 목록을 조회하는 함수
func listStudents() {
    print("학생 목록:")
    for (number, name) in STUDENTS {
        let studentSubjects = SUBJECTS[number]?.joined(separator: ", ") ?? "없음" // SUBJECTS[number]가 존재하지 않으면 "없음" 출력
        print("학생 번호: \(number), 이름: \(name), 듣는 과목: \(studentSubjects)")
    }
}

// 2. 학생 정보를 조회하는 함수
func viewStudent(number: String) {
    if let name = STUDENTS[number] { // STUDENTS[number]가 존재하면 name 상수에 할당, 아니면 else로
        let studentSubjects = SUBJECTS[number]?.joined(separator: ", ") ?? "없음" // subjects[number]가 존재하지 않으면 "없음" 출력
        print("학생 번호: \(number), 이름: \(name), 듣는 과목: \(studentSubjects)")

        for (subject, scores) in GRADES[number] ?? [:] { //각 과목에 대해 점수 Array를 나열, 과목이 존재하지 않으면 [:]로 처리
            print("\(subject) 성적: \(scores)")
        }
    } else {
        print("해당 번호의 학생이 존재하지 않습니다.")
    }
}

// 3. 학생을 등록하는 함수
func addStudent(number: String, name: String) {
    STUDENTS[number] = name
    SUBJECTS[number] = [] // 학생을 등록할 때 과목 Set도 초기화
    GRADES[number] = [:] // 성적 정보도 초기화
    print("\(number) 번호로 \(name) 학생이 등록되었습니다. )")
}

// II. 과목 관리

// 4. 과목 추가하는 함수
func addSubject(number: String, subject: String) {
    if var studentSubjects = SUBJECTS[number] { // 불러온 SUBJECTS[number]를 studentSubjects에 저장, 없으면 else로
        studentSubjects.insert(subject)
        SUBJECTS[number] = studentSubjects // 과목 추가한 studentSubjects를 SUBJECTS[number]에 저장
        GRADES[number]?[subject] = [] // GRADES -> [학생 번호:[과목:[점수]]] / 성적 정보 삭제
        print("\(STUDENTS[number] ?? "알 수 없는 학생")\(subject) 과목이 추가되었습니다.")
    } else {
        print("해당 번호의 학생이 존재하지 않습니다.")
    }
}

// 5. 과목 제거하는 함수
func removeSubject(number: String, subject: String) {
    if var studentSubjects = SUBJECTS[number] { // 불러온 SUBJECTS[number]를 studentSubjects에 저장, 없으면 else로
        if studentSubjects.remove(subject) != nil { // 여기서 != nil은 기능하는데 필요하진 않음. 다만 없는 과목을 삭제하려 해도 동작하는 게 문제임
            SUBJECTS[number] = studentSubjects // 과목 삭제한 studentSubjects를 SUBJECTS[number]에 저장
            GRADES[number]?.removeValue(forKey: subject) // GRADES -> [학생 번호:[과목:[점수]]] / 성적 정보 삭제
            print("\(STUDENTS[number] ?? "알 수 없는 학생")\(subject) 과목이 삭제되었습니다.")
        } else {
            print("\(subject) 과목이 존재하지 않습니다.")
        }
    } else {
        print("해당 번호의 학생이 존재하지 않습니다.")
    }
}

// III. 과목별 성적 관리

// 6. 과목별 성적 추가하는 함수
func addGrade(number: String, subject: String, grade: Int) {
    guard let _ = STUDENTS[number] else {
        print("해당 번호의 학생이 존재하지 않습니다.")
        return
    }

    guard let _ = GRADES[number]?[subject] else {
        print("\(subject) 과목이 존재하지 않거나 해당 학생이 없습니다.")
        return
    }

    GRADES[number]?[subject]?.append(grade)
    print("\(STUDENTS[number] ?? "알 수 없는 학생")\(subject) 과목에 성적 \(grade) 추가되었습니다.")
}

// 7. 과목별 성적 초기화하는 함수
func resetGrades(number: String, subject: String) {
    guard let _ = STUDENTS[number] else {
        print("해당 번호의 학생이 존재하지 않습니다.")
        return
    }

    guard GRADES[number]?[subject] != nil else {
        print("\(subject) 과목이 존재하지 않거나 해당 학생이 없습니다.")
        return
    }

    GRADES[number]?[subject] = [] // 성적 초기화
    print("\(STUDENTS[number] ?? "알 수 없는 학생")\(subject) 과목 성적이 초기화되었습니다.")
}

// IV. 성적 평균 계산

// 8. 학생의 각 과목 점수의 평균과 전체 평균을 계산하는 함수
func calculateAverageGrades(number: String) {
    guard let name = STUDENTS[number] else {
        print("해당 번호의 학생이 존재하지 않습니다.")
        return
    }

    guard let studentGrades = GRADES[number] else { // studentGrades에 GRADES[number] 담기
        print("\(name) 학생의 성적 정보가 없습니다.")
        return
    }

    var subjectAverages: [String: Double] = [:]

    for (subject, scores) in studentGrades { // GRADES[number]가 담겨 있는 studentGrades에 대해서 반복
        if !scores.isEmpty {
            let average = Double(scores.reduce(0, +)) / Double(scores.count)
            subjectAverages[subject] = average
        } else {
            subjectAverages[subject] = 0.0 // 성적이 없으면 평균을 0으로 설정
        }
    }

    // 각 과목 평균 출력
    print("\(name) 학생의 각 과목 평균:")
    for (subject, average) in subjectAverages {
        print("\(subject): \(average)")
    }

    // 전체 평균 계산
    let allAverages = subjectAverages.values
    let totalAverage = allAverages.isEmpty ? 0.0 : allAverages.reduce(0, +) / Double(allAverages.count) 

    print("\(name) 학생의 전체 평균: \(totalAverage)")
}

// 10. 명령어 도움말 함수
func showHelp() {
    print("""
    사용 가능한 명령어:
    1. listStudents: 등록된 학생 목록 조회
    2. viewStudent <번호>: 특정 학생 정보 조회
    3. addStudent <번호> <이름>: 학생 등록
    4. addSubject <번호> <과목>: 특정 학생에게 과목 추가
    5. removeSubject <번호> <과목>: 특정 학생에게 과목 제거
    6. addGrade <번호> <과목> <성적>: 특정 학생의 과목에 성적 추가
    7. resetGrades <번호> <과목>: 특정 학생의 과목 성적 초기화
    8. averageGrades <번호>: 특정 학생의 각 과목 성적 평균 및 전체 평균 계산
    9. help: 사용 가능한 명령어 열람
    0. exit: 프로그램 종료
    """)
}

// executeCommand 
func executeCommand(_ command: String) {
    let components = command.split(separator: " ").map { String($0) } // 문자열 command를 공백 기준으로 나누고 (배열 생성), 각 부분을 String 타입으로 변환 (map으로 또다른 배열 생성)

    guard let action = components.first else { // map으로 생성한 배열의 첫번째 String을 action에 저장.
        print("잘못된 명령어입니다.")
        return
    }

    switch action { // action이 무슨 명령어인지 파악
    case "listStudents":
        listStudents()

    case "viewStudent":
        if components.count == 2 {
            let number = components[1]
            viewStudent(number: number)
        } else {
            print("명령어 형식이 잘못되었습니다. 예: viewStudent 001")
        }

    case "addStudent":
        if components.count == 3 {
            let number = components[1]
            let name = components[2]
            addStudent(number: number, name: name)
        } else {
            print("명령어 형식이 잘못되었습니다. 예: addStudent 001 홍길동")
        }

    case "addSubject":
        if components.count == 3 {
            let number = components[1]
            let subject = components[2]
            addSubject(number: number, subject: subject)
        } else {
            print("명령어 형식이 잘못되었습니다. 예: addSubject 001 수학")
        }

    case "removeSubject":
        if components.count == 3 {
            let number = components[1]
            let subject = components[2]
            removeSubject(number: number, subject: subject)
        } else {
            print("명령어 형식이 잘못되었습니다. 예: removeSubject 001 수학")
        }

    case "addGrade":
        if components.count == 4 {
            let number = components[1]
            let subject = components[2]
            if let grade = Int(components[3]) {
                addGrade(number: number, subject: subject, grade: grade)
            } else {
                print("성적은 숫자로 입력해야 합니다.")
            }
        } else {
            print("명령어 형식이 잘못되었습니다. 예: addGrade 001 수학 85")
        }

    case "resetGrades":
        if components.count == 3 {
            let number = components[1]
            let subject = components[2]
            resetGrades(number: number, subject: subject)
        } else {
            print("명령어 형식이 잘못되었습니다. 예: resetGrades 001 수학")
        }

    case "averageGrades":
        if components.count == 2 {
            let number = components[1]
            calculateAverageGrades(number: number)
        } else {
            print("명령어 형식이 잘못되었습니다. 예: averageGrades 001")
        }

    case "help":
        showHelp()

    case "exit":
        print("프로그램을 종료합니다.")
        exit(0)

    default:
        print("알 수 없는 명령어입니다.")
    }
}

// 메인 루프: 사용자 입력을 계속 받아들임
while true {
    print("명령어를 입력하세요 (도움말은 'help' 입력): ", terminator: "")
    if let command = readLine() {
        executeCommand(command) // func executeCommand(_ command: String)에서 '_'를 붙였으니 'command: command'라 안적어도 됨
    }
}

만들면서 배운 내용

  • Foundation 프레임워크

    • 프레임워크: 특정 기능을 제공하는 라이브러리의 집합
    • Foundation 프레임워크: Swift와 Objective-C에서 기본적으로 제공되는 클래스와 기능 포함
      • 데이터 처리, 문자열 조작, 날짜 및 시간 관리, 네트워킹, 파일 시스템 작업 등과 같은 다양한 기본 기능 지원
  • func - 함수 정의하기

    • 기본 문법
      func functionName(parameters) -> ReturnType {
          // 코드 블록
      }
      • functionName: 함수의 이름
      • parameters: 함수에 전달할 입력값의 목록 - 각 매개변수는 이름과 타입을 명시해야 함
      • ReturnType: 함수가 반환하는 값의 타입 - 반환값이 없을 경우 Void로 명시하거나 생략할 수 있음
  • 옵셔널 체이닝(Optional Chaining) - ?

    • ?는 옵셔널 변수가 nil인지 아닌지를 확인한다.
    • 해당 값이 nil이 아니라면 그 뒤의 내용을 실행한다.
    • 해당 값이 nil이라면, 그 뒤의 내용을 실행하지 않고 nil을 반환한다.
    // 옵셔널 변수
    var optionalString: String? = "Hello"
    
    // 옵셔널 체이닝을 통한 문자열 길이 확인
    let length = optionalString?.count
    print(length) // Optional(5)
    
    // 옵셔널이 nil인 경우
    optionalString = nil
    let length2 = optionalString?.count
    print(length2) // nil
  • 널 병합 연산자 (??)

    • 옵셔널 값이 nil일 경우 대체 값을 제공하는 데 사용함
  • 문자열 연결하기

    • .joined(separator:): 배열의 문자열 요소를 연결하여 하나의 문자열로 만듦
      • separator: 파라미터를 이용해 각 문자열 요소를 나누는 문자열을 넣을 수 있음
  • 와일드카드 (_) 사용법

    • 옵셔널 바인딩에서 옵셔널 값이nil이 아닌지 확인할 때, _를 사용하면 그 값을 사용할 필요가 없다는 것을 나타낼 수 있음
    • 해당 값이 유효한지를 확인하기만 하고, 나중에 사용하지 않겠다는 의미
    • 코드의 가독성을 높일 수 있음
    guard let _ = students[number] else {
      print("해당 번호의 학생이 존재하지 않습니다.")
      return
    }
  • guard - 조건을 만족하지 않을 경우 즉시 실행을 중단하는 제어문

    • 코드의 가독성을 높임
    • 조건을 한 번에 여러 개 확인할 수 있음
    • guardelse 블록에서는 반드시 return, break, continue, throw 중 하나를 사용하여 제어 흐름을 변경해야 한다.
      guard 조건 else {
      // 조건이 false일 때 실행될 코드
      // 일반적으로 return, break, continue, throw 등이 사용됨
      }
  • return

    • 정의: return은 함수를 종료하고, 호출한 곳으로 제어를 반환하는 명령어이다.
      함수가 값을 반환하는 경우, return을 사용하여 값을 돌려준다.

    • 용도: 함수의 실행을 중단하고 값을 반환하거나, 값을 반환하지 않고 단순히 함수를 종료할 때 사용한다.

      func sayHello(to name: String) -> String {
        return "Hello, \(name)!" // "Hello, John!"과 같은 문자열을 반환
      }
      
      func printGreeting(name: String?) {
        guard let validName = name else {
            print("이름이 없습니다.")
            return // 함수 종료
        }
        print("안녕하세요, \(validName)님!")
      }
      
      print(sayHello(to: "John")) // "Hello, John!"
      printGreeting(name: nil)     // "이름이 없습니다." 
  • break

    • 정의: break는 반복문이나 switch문을 즉시 중단하고, 해당 블록을 빠져나가는 명령어이다.

    • 용도: 반복문(for, while, repeat)이나 switch문에서 현재 실행 중인 루프나 switch를 종료할 때 사용한다.

      let numbers = [1, 2, 3, 4, 5]
      
      for number in numbers {
        if number == 3 {
            print("3을 찾았습니다!")
            break // 반복문을 즉시 종료
        }
        print(number)
      }
  • continue

    • 정의: continue는 현재 반복문에서 남은 코드를 건너뛰고, 다음 반복을 진행하는 명령어이다.
    • 용도: 반복문 내에서 특정 조건에 따라 다음 반복으로 건너뛰고 싶을 때 사용한다.
      반복문은 종료되지 않지만, 현재 반복의 남은 코드가 실행되지 않고 넘어간다.
      for number in 1...5 {
          if number % 2 == 0 {
              continue // 짝수는 건너뛰고, 다음 반복으로
          }
          print("\(number)는 홀수입니다.")
      }
  • throw

    • 정의: thorw는 에러를 발생시키는 명령이다. 함수나 메소드가 에러를 던질 때 사용되며, 발생된 에러는 do-catch 구문을 통해 처리된다.

    • 용도: 에러가 발생할 수 있는 함수나 메소드에서 특정 상황에서 에러를 던지고, 호출한 코드에서 해당 에러를 처리할 수 있도록 한다.

      enum PasswordError: Error {
          case tooShort
          case tooSimple
      }
      
      func validatePassword(_ password: String) throws {
          if password.count < 6 {
              throw PasswordError.tooShort // 에러 던짐
          }
          if password == "123456" {
              throw PasswordError.tooSimple // 에러 던짐
          }
          print("비밀번호가 유효합니다.")
      }
      
      do {
          try validatePassword("123456") // 에러 발생
      } catch PasswordError.tooShort {
          print("비밀번호가 너무 짧습니다.")
      } catch PasswordError.tooSimple {
          print("비밀번호가 너무 단순합니다.")
      }
  • 클로저(Closures)

    • 익명 함수라고도 불리며, 코드에서 이름이 없는 짧은 함수나 블록을 말함
    • Swift에서 클로저는 함수와 거의 동일한 방식으로 동작
    • 클로저는 실행 가능한 코드 블록을 정의하고, 특정 컨텍스트 내에서 값을 캡처할 수 있음
      1. 입력 매개변수 / 2. 반환 타입 / 3. 실행 코드
    // 기본 문법
    { (parameters) -> returnType in
      code
    }
    // 예시
    let sumClosure = { (a: Int, b: Int) -> Int in
      return a + b
    }
    print(sumClosure(3, 5))  // 출력: 8
    • $0, $1 등의 사용
      • $0, $1, $2 등은 Swift 클로저에서 사용되는 익명 매개변수이다.
      • 클로저의 첫 번째, 두 번째, 세 번째 매개변수를 각각 참조할 때 사용하는 단축 문법임.
      • 코드를 간결하게 만드는 데 유용
      • 주로 map, filter, reduce 같은 고차 함수에서 사용됨
      • 주의: 클로저에서만 사용 가능한 문법임. 남용하면 가독성이 떨어질 수 있음.
  • reduce 메소드

    • 일종의 반복문처럼 작동함

    • 배열의 모든 요소를 결합해 단일 값으로 줄이는 데 사용

      let result = collection.reduce(initialResult) { (currentResult, element) in
        // 연산 수행
        return updatedResult
      }
      
      • collection: 누적 연산을 수행할 배열이나 컬렉션.
      • initialResult: 누적 연산의 초기값.
      • currentResult: 반환된 누적 결과(이전의 결과).
      • element: 현재 처리 중인 컬렉션의 요소.
      • updatedResult: 코드블록 내에서 계산된 새로운 결과, currentResult와 element를 바탕으로 한 값.
    • reduce 메소드는 클로저를 두 가지 방식으로 사용할 수 있음

      1. 명시적 클로저 사용 버전:
        let sum = numbers.reduce(0) { $0 + $1 }
        • 클로저를 직접 정의하는 방식.
        • { $0 + $1 }는 명시적 클로저로, 배열의 각 요소를 차례로 처리하는 방법을 지정함
          • $0은 현재까지 계산된 결과(누적값)
          • $1은 배열의 현재 요소

      1. 연산자 클로저 축약 버전:
        let sum = numbers.reduce(0, +)
      • 첫 번째 인자: 0은 초기 값. (만약 곱셈을 하고 싶다면 1을 놔야할 것)
      • 두 번째 인자: +는 함수처럼 동작하는 연산자임. + 연산자는 사실 func +(lhs: Int, rhs: Int) -> Int와 같은 함수 형태로 존재함 (참고: 연산자 계산 시 자주 쓰는 이름 / lhs left-hand side, rhs: right-hand side)
        let sum = numbers.reduce(0, { (currentSum: Int, number: Int) -> Int in
        return currentSum + number
        })
        // 0: 초기값. currentSum 에 전달됨
        // 클로저: (currentSum, number)의 두 매개변수를 받고, 두 값을 더해 반환하는 코드

        let numbers = [1, 2, 3, 4, 5]
        let sum = numbers.reduce(0) { (result, number) in
         return result + number
        }
        print(sum) // 출력: 15
        위의 코드를 클로저 대신 연산자를 사용하면 간단하게 표현할 수 있음
        let sum = numbers.reduce(0, +)
        print(sum) // 출력: 15
  • reduce(0, +) 분석

    • 첫 번째 인자: 0은 초기 값. (만약 곱셈을 하고 싶다면 1을 놔야할 것)
    • 두 번째 인자: +는 함수처럼 동작하는 연산자임. + 연산자는 사실 func +(lhs: Int, rhs: Int) -> Int와 같은 함수 형태로 존재함 (연산자 계산 시 자주 쓰는 이름 / lhs: left-hand side, rhs: right-hand side)
    let sum = numbers.reduce(0, { (currentSum: Int, number: Int) -> Int in
      return currentSum + number
    })
    // 0: 초기값. currentSum 에 전달됨
    // 클로저: (currentSum, number)의 두 매개변수를 받고, 두 값을 더해 반환하는 코드
  • 파라미터(parameter)와 인자(argument)의 차이

    • 파라미터(parameter)
      • 정의: 함수나 메서드를 정의할 때 함수의 입력으로 사용되는 변수를 파라미터라고 함
      • 위치: 함수 선언 부분에 등장
      func add(a: Int, b: Int) -> Int {
      return a + b 
      }
      // a와 b는 파라미터. 함수가 호출될 때 어떤 값을 받을지 정의한 변수.
    • 인자(argument)
      • 정의: 함수나 메서드를 호출할 때, 실제로 전달되는 값을 인자라고 함
      • 위치: 함수 호출 부분에 등장
      let result = add(a: 3, b: 5)
      // `3`과 `5`는 인자. 함수가 실행될 때 함수에 실제로 전달되는 값.
  • 삼항 연산자(ternary operator)

    • 가독성과 간결성을 제공하는 문법
    • 복잡한 로직에선 사용을 자제하는 게 좋음
    condition ? valueIfTrue : valueIfFalse
    • condition: 평가할 조건.
    • valueIfTrue: 조건이 참일 때 반환할 값.
    • valueIfFalse: 조건이 거짓일 때 반환할 값.
  • 매개변수 이름의 두 가지 종류

    • 외부 매개변수 이름 (external parameter name): 함수 호출 시 사용하는 이름.
    • 내부 매개변수 이름 (internal parameter name): 함수 정의 안에서 사용하는 이름.
  • 매개변수 앞에 _ 붙이기

    • 매개변수(parameter) 앞에 _를 붙이면 외부 매개변수의 이름을 생략할 수 있다.
    • 여러 개 붙일 경우에는 호출 시 순서를 잘 기억해야 하니 주의한다.
    func exampleFunction(_ firstName: String, lastName: String) {
      print("First name: \(firstName), Last name: \(lastName)")
    }
    exampleFunction("John", lastName: "Doe")
  • split 메소드

    • .split(separator:)은 문자열을 특정 구분자(delimiter)로 나눠 배열로 반환함
    • 기본적으로 Substring 배열로 반환됨
  • Substring 타입

    • SubstringString의 부분 문자열을 나타내는 타입
    • String의 일부를 메모리에서 효율적으로 참조 -> String으로 쓰기에는 변환이 필요함
  • map 메소드

    • 배열이나 컬렉션의 각 요소에 특정 함수를 적용해 새로운 배열을 생성함
    • 원본 배열은 변경되지 않음
    • 배열의 크기 및 요소 개수를 유지하면서, 각 요소를 변환해 반환함.
    • nil 요소에 대한 처리가 불가능함. 옵셔널 처리가 필요하면 compactMap을 써야 한다.
    // 기본 문법
    let newArray = oldArray.map { (element) in
      // 변환 로직
      return 변환된 값
    }

    let numbers = [1, 2, 3, 4, 5]
    // 각 요소를 2배로 변환하는 map
    let doubled = numbers.map { (num) in
        return num * 2
    }
    
    print(doubled) // 출력: [2, 4, 6, 8, 10]

    클로저의 익명 매개변수를 사용해 더 간결하게 작성할 수 있다.
    $0은 배열의 각 요소를 의미한다.

    let doubled = numbers.map { $0 * 2 }
    print(doubled) // 출력: [2, 4, 6, 8, 10]
  • while true로 무한 루프를 생성할 수 있다.

  • readLine()을 통해 콘솔에서 상호작용하는 애플리케이션을 만들 수 있다.

    • readLine()의 반환값은 String이 아니라 String?
    • 따라서 if let 등으로 옵셔널 처리를 해줘야 함
  • print()에서 terminator의 역할

    • 기본적으로 print는 출력 후에 새 줄(\n)을 추가하지만, terminator를 사용하여 이 기본 동작을 변경할 수 있다.
  • print("""...""")로 문자열 블록 입력하기 (멀티라인 문자열)

    1. 멀티라인 문자열 생성

      let multilineString = """
      안녕하세요!
      이것은 멀티라인 문자열입니다.
      여러 줄로 구성되어 있습니다.
      """
      
      print(multilineString)
    2. 문자열 내부의 특수 문자 처리

      • 자동 줄바꿈: 멀티라인 문자열은 작성한 그대로 줄바꿈을 포함한다.
      • 백슬래시 처리: 문자열 내에서 \n, \t와 같은 특수 문자를 사용하지 않고도 줄바꿈과 공백을 쉽게 표현할 수 있다.
    3. 문자열 앞에 공백을 포함한 경우

      • 문자열 블록의 각 줄 앞에 있는 공백은 문자열에 포함된다.
      • 원하지 않는 공백을 제거하려면 trimmingCharacters(in:) 메소드를 사용할 수 있다.
      let indentedString = """
        Hello, 
        This is a multiline string 
        with indentation.
        """.trimmingCharacters(in: .whitespacesAndNewlines)
      
      print(indentedString)
      • trimmingCharacters(in:) 메소드에 대한 공부는 일단 생략...
  • exit() 함수

    • exit(0): 정상적으로 프로그램 종료 (exit()exit(0)으로 간주됨)
    • exit(1): 오류가 발생하여 프로그램 비정상 종료
profile
Reciprocity lies in knowing enough

0개의 댓글