From f43d84164ac34613c192f8dee71c5c85af8b1066 Mon Sep 17 00:00:00 2001 From: Sei Kim Date: Tue, 20 Dec 2022 10:13:43 +0900 Subject: [PATCH] =?UTF-8?q?credit=20manager=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CreditManager.swift | 267 ++++++++++++++++++++++++++++++++++++++++++++ Error.swift | 56 ++++++++++ IOManager.swift | 44 ++++++++ Info.swift | 63 +++++++++++ Score.swift | 68 +++++++++++ Status.swift | 114 +++++++++++++++++++ Student.swift | 48 ++++++++ main.swift | 11 ++ 8 files changed, 671 insertions(+) create mode 100644 CreditManager.swift create mode 100644 Error.swift create mode 100644 IOManager.swift create mode 100644 Info.swift create mode 100644 Score.swift create mode 100644 Status.swift create mode 100644 Student.swift create mode 100644 main.swift diff --git a/CreditManager.swift b/CreditManager.swift new file mode 100644 index 0000000..3d9caa3 --- /dev/null +++ b/CreditManager.swift @@ -0,0 +1,267 @@ +// +// CreditManager.swift +// Git_ Exercise +// +// Created by sei_dev on 12/19/22. +// + +import Foundation + +/// creditManager의 DataFile 관련 정보들 +enum DataFile { + /// 데이터가 저장되고 로드하는 json 파일 이름 + static let name = "data.json" + /// 파일이 저장될 Directory + static let directory: String = FileManager.default.currentDirectoryPath + static let pathString = DataFile.directory.appending(DataFile.name) + static var pathUrl: URL { + if #available(macOS 13.0, *) { + return URL(filePath: DataFile.directory.appending(DataFile.name)) + } else { + return URL(fileURLWithPath: DataFile.directory.appending(DataFile.name)) + } + } +} + +final class CreditManager { + /// CreditManager - singleton + static let shared = CreditManager() + private init() { } + + /// CreditManager의 현재 status + private var status: Status = .start + /// CreditManager에 등록된 Student의 List + private var students = [Student]() + + /// 1. 저장된 데이터 로드 + /// 2. 프로그램 상태에 맞는 info 메세지 출력 + /// 3. input 처리 및 실행 + /// 4. error발생 시 설명 출력 + /// - 종료 error 시 데이터 저장 및 프로그램 종료 + func run() { + loadData() + while true { + do { + IOManager.writeMessage(status.infoMessage) + let input = try IOManager.getInput(isStartStatus: status == .start) + let parsedInput = try parse(input: input) + try doWith(parsedInput) + } catch { + IOManager.writeMessage(error.localizedDescription, type: .error) + switch error { + case CMError.quitProgram: + saveData() + return + default: + status = .start + } + } + } + } + + /// status 별 input parse + private func parse(input: String) throws -> ParsedInput { + guard let parsedInput = status.parse(input: input) else { + throw status == .start ? CMError.invalidStartInput : IOError.wrongInput + } + return parsedInput + } + + + /// 입력과 상태에 따라 CreditManager 작동 분기 + /// + /// - result Infomation이 있다면 출력한다 + /// - 실행 후 credit manager의 status는 start로 변경 + private func doWith(_ input: ParsedInput) throws { + do { + switch self.status { + case .start: + try start(input) + return + case .addStudent: + try add(student: input) + case .deleteStudent: + try delete(student: input) + case .addScore: + try add(score: input) + case .deleteScore: + try delete(score: input) + case .showScoreAverage: + try show(score: input) + case .exit: + return + } + } catch { + throw error + } + if let resInfo = status.resMessage(input: input) { + IOManager.writeMessage(resInfo, type: .reaction) + } + status = .start + } +} + +//MARK: - Start + +extension CreditManager { + /// 입력에 따라 credit manager의 status 변경 + private func start(_ input: ParsedInput) throws { + guard let nextStatus = input[0] as? Status else { + throw CMError.invalidStartInput + } + guard nextStatus != .exit else { + throw CMError.quitProgram + } + guard [Status.start, Status.addStudent].contains(nextStatus) || false == students.isEmpty else { + throw CMError.emptyStudents + } + self.status = nextStatus + } +} + +//MARK: - AddStudent + +extension CreditManager { + /// 입력받은 학생을 students에 추가 + /// + /// Error occurs when + /// - 입력받은 학생이 이미 존재 + private func add(student input: ParsedInput) throws { + guard let name = input[0] as? String else { + throw IOError.wrongInput + } + guard false == exists(student: name) else { + throw CMError.studentAleadyExists(name: name) + } + let student = Student(name: name) + students.append(student) + } + + /// 학생이 등록되어 있다면 true + private func exists(student name: String) -> Bool { + return students.contains { $0.name == name } + } +} + +//MARK: - Delete Student + +extension CreditManager { + /// 입력받은 학생을 students에서 삭제 + /// + /// Error occurs when + /// - 등록된 학생이 0명 + /// - 입력받은 학생이 존재하지 않음 + private func delete(student input: ParsedInput) throws { + guard false == students.isEmpty else { + throw CMError.emptyStudents + } + guard let name = input[0] as? String else { + throw IOError.wrongInput + } + guard exists(student: name) else { + throw CMError.studentNotFound(name: name) + } + students.removeAll { $0.name == name } + } +} + + +//MARK: - Add Score + +extension CreditManager { + /// 등록된 학생에 course, score 정보 추가 + /// + /// Error occurs when + /// - 입력받은 학생이 존재하지 않음 + private func add(score input: ParsedInput) throws { + guard let name = input[0] as? String, + let course = input[1] as? String, + let score = input[2] as? Score else { + throw IOError.wrongInput + } + guard let student = students.first(where: { $0.name == name}) else { + throw CMError.studentNotFound(name: name) + } + student.update(course: course, score: score) + } +} + +//MARK: - Delete Score + +extension CreditManager { + /// 등록된 학생의 course 정보 삭제 + /// + /// Error occurs when + /// - 입력받은 학생이 존재하지 않음 + /// - 학생 정보에 해당 과목이 등록되어 있지 않음 + private func delete(score input: ParsedInput) throws { + guard let name = input[0] as? String, + let course = input[1] as? String else { + throw IOError.wrongInput + } + guard let student = students.first(where: { $0.name == name}) else { + throw CMError.studentNotFound(name: name) + } + guard student.remove(course: course) != nil else { + throw CMError.courseNotFound(name: name, course: course) + } + } +} + +//MARK: - Show Average Score + +extension CreditManager { + /// 등록된 학생의 전체 과목과 평점 확인 + /// + /// Error occurs when + /// - 입력받은 학생이 존재하지 않음 + /// - 학생 정보에 등록된 과목이 없음 + private func show(score input: ParsedInput) throws { + guard let name = input[0] as? String else { + throw IOError.wrongInput + } + guard let student = students.first(where: { $0.name == name}) else { + throw CMError.studentNotFound(name: name) + } + guard false == student.scores.isEmpty else { + throw CMError.emptyCourse(name: name) + } + IOManager.writeMessage(student.allScoresDescription) + } +} + + +extension CreditManager { + /// students property를 json 파일에 저장 + private func saveData() { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + do { + let data = try encoder.encode(StudentList(students: students)) + + if false == FileManager.default.fileExists(atPath: DataFile.pathString) { + FileManager.default.createFile(atPath: DataFile.pathString, contents: nil) + } + + try data.write(to: DataFile.pathUrl) + let names = students.map {$0.name} + + IOManager.writeMessage(Info.Data.saved(names: names), type: .reaction) + } catch { + IOManager.writeMessage(error.localizedDescription, type: .error) + } + } + + /// json 파일 데이터를 students property에 불러오기 + private func loadData() { + guard let data = try? Data(contentsOf: DataFile.pathUrl), + let studentList = try? JSONDecoder().decode(StudentList.self, from: data) else { + IOManager.writeMessage(Info.Warning.failedToLoadData, type: .error) + return + } + students = studentList.students + + let names = students.map {$0.name} + IOManager.writeMessage(Info.Data.loaded(names: names), type: .reaction) + } +} diff --git a/Error.swift b/Error.swift new file mode 100644 index 0000000..34f38f0 --- /dev/null +++ b/Error.swift @@ -0,0 +1,56 @@ +// +// Error.swift +// Git_ Exercise +// +// Created by sei_dev on 12/19/22. +// + +import Foundation + +//MARK: - CMError + +enum CMError: Error { + case invalidStartInput + case studentAleadyExists(name: String) + case studentNotFound(name: String) + case emptyCourse(name: String) + case courseNotFound(name: String, course:String) + case emptyStudents + case quitProgram +} + +extension CMError: LocalizedError { + public var errorDescription: String? { + switch self { + case .invalidStartInput: + return "입력이 잘못되었습니다. 1~5 사이의 숫자 혹은 X를 입력해주세요." + case let .studentAleadyExists(name): + return "\(name): 이미 존재하는 학생입니다. 추가하지 않습니다." + case let .studentNotFound(name): + return "\(name) 학생을 찾지 못했습니다." + case let .courseNotFound(name, course): + return "\(name) - \(course): 등록되지 않은 과목입니다." + case let .emptyCourse(name): + return "\(name): 등록된 과목이 없습니다." + case .emptyStudents: + return "등록된 학생이 존재하지 않습니다." + case .quitProgram: + return "프로그램을 종료합니다..." + } + } +} + +//MARK: - IOError + +enum IOError: Error { + case wrongInput +} + +extension IOError: LocalizedError { + public var errorDescription: String? { + switch self { + case .wrongInput: + return "입력이 잘못되었습니다. 다시 확인해주세요." + } + } +} diff --git a/IOManager.swift b/IOManager.swift new file mode 100644 index 0000000..d8ec114 --- /dev/null +++ b/IOManager.swift @@ -0,0 +1,44 @@ +// +// IOManager.swift +// Git_ Exercise +// +// Created by sei_dev on 12/19/22. +// + +import Foundation + +/// Console output type +enum OutputType { + case information + case reaction + case error +} + +enum IOManager { + static let nameRegex = "^[A-Za-z0-9_]+$" + + /// 사용자 입력 받기 + static func getInput(isStartStatus: Bool) throws -> String { + guard let input = readLine(), + input != "" else { + throw isStartStatus ? CMError.invalidStartInput : IOError.wrongInput + } + return input + } + + static func checkValidity(of str: String) -> Bool { + return str.range(of: Self.nameRegex, options: .regularExpression, range: nil, locale: nil) != nil + } + + /// message 콘솔에 출력 + static func writeMessage(_ message: String, type: OutputType = .information) { + switch type { + case .information: + print("> \(message)") + case .reaction: + print("< \(message)") + case .error: + fputs("⚠️ \(message)\n", stderr) + } + } +} diff --git a/Info.swift b/Info.swift new file mode 100644 index 0000000..77bfaea --- /dev/null +++ b/Info.swift @@ -0,0 +1,63 @@ +// +// Info.swift +// Git_ Exercise +// +// Created by sei_dev on 12/19/22. +// + +import Foundation + +enum Info { + static let start = """ + 원하는 기능을 입력해주세요. + 1: 학생 추가, 2: 학생 삭제, 3: 성적 추가(변경), 4: 성적 삭제, 5: 평점 보기, X: 종료 + """ + enum Student { + static let forAdd = "추가할 학생의 이름을 입력해주세요." + static func added(name: String) -> String { + return "\(name) 학생을 추가했습니다." + } + static let forDelete = "삭제할 학생의 이름을 입력해주세요." + static func deleted(name: String) -> String { + return "\(name) 학생을 삭제했습니다." + } + } + enum Score { + static let forAdd = """ + 성적을 추가할 학생의 이름, 과목 이름, 성적(A+, A, F 등)을 띄어쓰기로 구분하여 차례로 작성해주세요. + 입력 예) Mickey Swift A+ + 학생의 성적 중 해당 과목이 존재하면 기존 점수가 갱신됩니다. + """ + static func added(name:String, course: String, score: String) -> String { + return "\(name) 학생의 \(course) 과목이 \(score)로 추가(변경)되었습니다." + } + static let forDelete = """ + 성적을 삭제할 학생의 이름, 과목 이름을 띄어쓰기로 구분하여 차례로 작성해주세요. + 입력 예) Mickey Swift + """ + static func deleted(name:String, course:String) -> String { + return "\(name) 학생의 \(course) 과목의 성적이 삭제되었습니다." + } + static let forAverage = "평점을 알고 싶은 학생의 이름을 입력해주세요." + } + enum Data { + static func saved(names: [String]) -> String { + return names.isEmpty ? "저장할 학생 정보가 없습니다." + : """ + 학생과 성적 정보를 성공적으로 저장했습니다. + 저장된 학생: \(names.joined(separator: ", ")) + """ + } + static func loaded(names: [String]) -> String { + return """ + 학생과 성적 정보를 성공적으로 불러왔습니다. + 불러온 학생: \(names.joined(separator: ", ")) + """ + } + } + + enum Warning { + static let failedToLoadData = "기존의 Data를 불러오는 데 실패했습니다." + static let failedToLoadReaction = "실행 결과 안내 메세지를 불러오는 데 실패했습니다." + } +} diff --git a/Score.swift b/Score.swift new file mode 100644 index 0000000..ba572c4 --- /dev/null +++ b/Score.swift @@ -0,0 +1,68 @@ +// +// Score.swift +// Git_ Exercise +// +// Created by sei_dev on 12/19/22. +// + +import Foundation + +enum Score: Float, Codable { + case APlus = 4.5 + case A = 4 + case BPlus = 3.5 + case B = 3 + case CPlus = 2.5 + case C = 2 + case DPlus = 1.5 + case D = 1 + case F = 0 + + var description: String { + switch self { + case .APlus: + return "A+" + case .A: + return "A0" + case .BPlus: + return "B+" + case .B: + return "B0" + case .CPlus: + return "C+" + case .C: + return "C0" + case .DPlus: + return "D+" + case .D: + return "D0" + case .F: + return "F" + } + } + + init?(score: String) { + switch score { + case "A+", "a+", "4.5": + self = .APlus + case "A", "a", "A0", "a0", "4", "4.0": + self = .A + case "B+", "b+", "3.5": + self = .BPlus + case "B", "b", "B0", "b0", "3", "3.0": + self = .B + case "C+", "c+", "2.5": + self = .CPlus + case "C", "c", "C0", "c0", "2", "2.0": + self = .C + case "D+", "d+", "1.5": + self = .DPlus + case "D", "d", "D0", "d0", "1", "1.0": + self = .D + case "F", "f", "0", "0.0": + self = .F + default: + return nil + } + } +} diff --git a/Status.swift b/Status.swift new file mode 100644 index 0000000..5a332b1 --- /dev/null +++ b/Status.swift @@ -0,0 +1,114 @@ +// +// Status.swift +// Git_ Exercise +// +// Created by sei_dev on 12/19/22. +// + +import Foundation + +typealias ParsedInput = [Codable] + +/// status 정보 +enum Status: String, Codable { + case start = "0" + case addStudent = "1" + case deleteStudent = "2" + case addScore = "3" + case deleteScore = "4" + case showScoreAverage = "5" + case exit = "X" +} + +//MARK: - Info Message + +extension Status { + var infoMessage: String { + switch self { + case .start: + return Info.start + case .addStudent: + return Info.Student.forAdd + case .deleteStudent: + return Info.Student.forDelete + case .addScore: + return Info.Score.forAdd + case .deleteScore: + return Info.Score.forDelete + case .showScoreAverage: + return Info.Score.forAverage + default: + return "" + } + } +} + +//MARK: - Result Message + +extension Status { + func resMessage(input: ParsedInput) -> String? { + switch self { + case .addStudent: + if let name = input[0] as? String { + return Info.Student.added(name: name) + } + case .deleteStudent: + if let name = input[0] as? String { + return Info.Student.deleted(name: name) + } + case .addScore: + if let name = input[0] as? String, + let course = input[1] as? String, + let score = input[2] as? Score { + return Info.Score.added(name: name, course: course, score: score.description) + } + case .deleteScore: + if let name = input[0] as? String, + let course = input[1] as? String { + return Info.Score.deleted(name: name, course: course) + } + default: + return nil + } + return Info.Warning.failedToLoadReaction + } +} + +//MARK: - Status - parse method + +extension Status { + /// status에 따라 사용자 입력 parse + func parse(input: String) -> ParsedInput? { + let splited = input.components(separatedBy: " ") + switch (self, splited.count) { + case (.start, 1): + guard let status = Status(rawValue: splited[0]) else { + return nil + } + return [status] + case (.addStudent, 1), (.deleteStudent, 1), (.showScoreAverage, 1): + guard IOManager.checkValidity(of: splited[0]) else { + return nil + } + return splited + case (.addScore, 3): + guard IOManager.checkValidity(of: splited[0]), + IOManager.checkValidity(of: splited[1]), + let score = Score(score: splited[2]) else { + return nil + } + var res = [Codable]() + res.append(contentsOf: splited) + res[2] = score + return res + case (.deleteScore, 2): + guard IOManager.checkValidity(of: splited[0]), + IOManager.checkValidity(of: splited[1]) else { + return nil + } + return splited + default: + return nil + } + } +} diff --git a/Student.swift b/Student.swift new file mode 100644 index 0000000..245d355 --- /dev/null +++ b/Student.swift @@ -0,0 +1,48 @@ +// +// Student.swift +// Git_ Exercise +// +// Created by sei_dev on 12/19/22. +// + +import Foundation + +final class Student: Codable { + let name: String + private(set) var scores = [String:Score]() + + var isCourseEmpty: Bool { + return scores.isEmpty + } + + var averageScore: Float { + let total = scores.values.reduce(0) { $0 + $1.rawValue } + return total/Float(scores.values.count) + } + + var allScoresDescription: String { + var res = "" + for item in scores { + res += "\(item.key): \(item.value.description)\n" + } + return res + "평점: " + String(format: "%.2f", averageScore) + } + + init(name: String) { + self.name = name + } + + func update(course: String, score: Score) { + scores.updateValue(score, forKey: course) + } + + func remove(course: String) -> Score? { + return scores.removeValue(forKey: course) + } +} + +//MARK: - Student DataStructure + +struct StudentList: Codable { + let students: [Student] +} diff --git a/main.swift b/main.swift new file mode 100644 index 0000000..11d507d --- /dev/null +++ b/main.swift @@ -0,0 +1,11 @@ +// +// main.swift +// Git_ Exercise +// +// Created by sei_dev on 2022/12/18. +// + +import Foundation + +let cm = CreditManager.shared +cm.run()