[TECHIT] 코틀린 11

hegleB·2023년 5월 26일
0
post-thumbnail

두 번째 미니 프로젝트로 운동 기록을 작성하고 날짜별로 작성된 기록을 출력하는 프로그램이다.

메뉴를 선택해주세요
1. 오늘의 운동 기록
2. 날짜별 운동 기록 보기
3. 종료
번호 입력 : 1
운동 종류 : 숨쉬기
횟수 : 3000
세트 : 3
메뉴를 선택해주세요
1. 오늘의 운동 기록
2. 날짜별 운동 기록 보기
3. 종료
번호 입력 : 2
1 : 2023-05-22
2 : 2023-05-23
3 : 2023-05-24
4 : 2023-05-25
날짜를 선택해주세여(0. 이전) : 1
2023-05-22의 운동 기록입니다
운동 타입 : 운동1
횟수 : 3
세트 : 3
운동 타입 : 운동2
횟수 : 100
세트 : 2
운동 타입 : 운동3
횟수 : 40
세트 : 10

[내가 작성한 코드]

import java.io.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*

fun main() {
    val mainClass = MainClass()
    mainClass.run()
}


class MainClass {

    private var programState = ProgramState.PROGRAM_EXERCISE_MENU_INPUT

    // 프로그램 실행
    fun run() {
        val exercise = Exercise()

        while (true) {
            when (programState) {
                ProgramState.PROGRAM_EXERCISE_MENU_INPUT -> {
                    startInputMenu()
                }

                ProgramState.PROGRAM_EXERCISE_RECORD_INPUT -> {
                    startInputExerciseRecord(exercise)
                }

                ProgramState.PROGRAM_EXERCISE_RECORD_PRINT -> {
                    startPrintExerciseRecord(exercise)
                }

                ProgramState.PROGRAM_EXERCISE_RECORD_QUIT -> break
            }
        }
    }

    private fun startInputExerciseRecord(exercise: Exercise) {
        exercise.inputExercise()
        programState = ProgramState.PROGRAM_EXERCISE_MENU_INPUT
    }

    private fun startPrintExerciseRecord(exercise: Exercise) {
        try {
            val number = exercise.inputDate()
            if (number == 0) {
                programState = ProgramState.PROGRAM_EXERCISE_MENU_INPUT
            } else {
                exercise.printExercise(number)
                programState = ProgramState.PROGRAM_EXERCISE_MENU_INPUT
            }
        } catch (e: Exception) {
            println("[ERROR] 날짜 선택을 잘못 입력하였습니다.")
            programState = ProgramState.PROGRAM_EXERCISE_RECORD_PRINT
        }
    }

    private fun startInputMenu() {
        try {
            val menu = Menu()
            programState = menu.navigateToSelectedMenu()
        } catch (e: Exception) {
            println(e.message)
            return
        }
    }
}

// 프로그램 상태
enum class ProgramState {
    PROGRAM_EXERCISE_MENU_INPUT,
    PROGRAM_EXERCISE_RECORD_INPUT,
    PROGRAM_EXERCISE_RECORD_PRINT,
    PROGRAM_EXERCISE_RECORD_QUIT
}

// 메뉴
class Menu {
    // 메뉴 입력
    private fun inputMenu(scanner: Scanner): Int {
        println()
        println("메뉴를 선택해주세요")
        println("1. 오늘의 운동 기록")
        println("2. 날짜별 운동 가록 보기")
        println("3. 종료")
        print("번호 입력 : ")
        return scanner.nextInt()
    }

    fun navigateToSelectedMenu(): ProgramState {
        val scanner = Scanner(System.`in`)
        var menuItem = 0
        try {
            menuItem = inputMenu(scanner)
        } catch (e: Exception) {
            println(e.message)
        }
        println()
        return when (menuItem) {
            1 -> ProgramState.PROGRAM_EXERCISE_RECORD_INPUT
            2 -> ProgramState.PROGRAM_EXERCISE_RECORD_PRINT
            3 -> ProgramState.PROGRAM_EXERCISE_RECORD_QUIT
            else -> throw Exception("[ERROR] 메뉴 선택을 잘못하였습니다. 다시 입력해주세요.")
        }
    }
}

// 파일관리
class FileHandler {
    private val file = File("ExerciseRecord.data")

    fun saveExerciseRecord(exerciseRecord: ExerciseRecord) {

        try {
            val records = getExerciseRecords().toMutableList()
            records.add(exerciseRecord)
            val oos = ObjectOutputStream(FileOutputStream(file))
            oos.writeObject(records)
            oos.close()
        } catch (e: Exception) {
            println(e.message)
        }
    }

    fun getExerciseRecords(): List<ExerciseRecord> {
        val records: List<ExerciseRecord>

        try {
            val ois = ObjectInputStream(FileInputStream(file))
            records = ois.readObject() as List<ExerciseRecord>
            ois.close()
        } catch (e: FileNotFoundException) {
            return emptyList()
        } catch (e: Exception) {
            println(e.message)
            return emptyList()
        }

        return records
    }
}

// 운동
class Exercise {

    private val file = FileHandler()
    private val dateMap = mutableMapOf<String, List<ExerciseRecord>>()
    private val scanner = Scanner(System.`in`)

    // 운동 기록 입력
    fun inputExercise() {
        val date = Date()
        print("운동 종류 : ")
        val type = scanner.next()
        val count = inputInt("세트")
        val set = inputInt("횟수")
        file.saveExerciseRecord(ExerciseRecord(date.getToday(), type, count, set))
    }

    private fun inputInt(inputType: String): Int {
        while (true) {
            try {
                print("${inputType} : ")
                val set = scanner.nextInt()
                return set
            } catch (e: InputMismatchException) {
                println("[ERROR] 숫자만 입력해주세요")
                scanner.nextLine()
            }
        }
    }

    fun inputDate(): Int {
        filterRecord()
        for (i in 1..dateMap.size) {
            if (dateMap.isNotEmpty()) {
                println("${i}. ${dateMap.keys.toList().get(i - 1)}\n")
            }
        }
        print("날짜를 선택해주세요(0. 이전) : ")
        return scanner.nextInt()
    }

    fun printExercise(number: Int) {
        val date = dateMap.keys.toList().get(number - 1)
        println()
        println("${date}의 운동 기록입니다.\n")
        if (dateMap.isNotEmpty()) {
            for (record in dateMap[date] ?: emptyList()) {
                println("운동 타입 : ${record.type}")
                println("횟수 : ${record.count}")
                println("세트 : ${record.set}\n")
            }
        }
    }

    fun filterRecord() {
        val records = file.getExerciseRecords()
        for (i in 0 until records.size) {
            val recordList = records.filter { it.date == records[i].date }
            dateMap.put(records[i].date, recordList)
        }
    }
}

class Date {
    fun getToday(): String {
        val currentDate = LocalDate.now()
        val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
        val formattedDate = currentDate.format(formatter)
        return formattedDate
    }
}

// 운동 기록
data class ExerciseRecord(val date: String, val type: String, val count: Int, val set: Int) : Serializable

처음에 Menu, FileHandler, Exercise, Date 클래스로 나누었고, 데이터를 보관하기 위해 ExerciseRecord 데이터 클래스를 추가하였다. 그리고 난 후 예외 처리에 대해 생각하였다.
예외 처리는 메뉴 입력 부분에서 숫자를 입력하지 않은 경우, 기록 입력하는 부분에서 숫자를 입력하지 않은 경우, 날짜 선택에서 숫자를 입력하지 않은 경우try-catch를 사용하여 예외를 처리하였다.
현재 날짜를 기준으로 기록을 남겨야 하기 때문에 LocalDate를 사용하여 현재 날짜를 가져오도록 구현하였다. 또한 기록을 파일을 생성한 후 ExerciseRecord 데이터 클래스에 Serializable를 사용하여 직렬화를 하고 파일에 데이터를 저장하도록 하였다.

[강사님이 작성한 코드]

// main.kt
import java.io.FileInputStream
import java.io.ObjectInputStream
import java.io.Serializable
import java.util.Scanner
import kotlin.system.exitProcess

fun main(){
    val mainClass = MainClass()
    mainClass.running()

}

// 메인 클래스
class MainClass{

    val scanner = Scanner(System.`in`)

    // 각 상태별 객체를 생성한다.
    val mainMenuClass = MainMenuClass(scanner)
    val inputRecordClass = InputRecordClass(scanner, this)
    val showRecordClass = ShowRecordClass(scanner, this)

    // 프로그램 상태를 담는 변수에 초기 상태를 설정한다.
    var programState = ProgramState.PROGRAM_STATE_SHOW_MENU

    // 기록된 운동을 보는 상태 변수
    lateinit var showRecordState:ShowRecordState

    // 운동 기록 정보를 담을 클래스
    data class RecordClass(var type:String, var count:Int, var set:Int) :Serializable

    // 운동 기록을 담을 리스트
    val recordList = mutableListOf<RecordClass>()
    // 파일 목록을 담을 리스트
    val recordFileList = mutableListOf<String>()


    // 프로그램 상태 전체를 관리하며 운영하는 메서드
    fun running(){
        while(true) {
            // 프로그램 상태에 따른 분기
            when (programState) {
                // 메인 메뉴를 보여주는 상태
                ProgramState.PROGRAM_STATE_SHOW_MENU ->{
                    // 메인 메뉴를 보여준다.
                    val inputMainMenuNumber = mainMenuClass.inputMainMenuNumber()
                    // println(inputMainMenuNumber)
                    // exitProcess(0)

                    when(inputMainMenuNumber){
                        // 오늘의 운동 기록 메뉴
                        MainMenuItem.MAIN_MENU_ITEM_WRITE_TODAY_RECORD.itemNumber -> {
                            // 운동 기록 상태로 바꾼다.
                            programState = ProgramState.PROGRAM_STATE_WRITE_TODAY_RECORD
                        }
                        // 운동 기록 보기 메뉴
                        MainMenuItem.MAIN_MENU_ITEM_SHOW_RECORD.itemNumber -> {
                            // 운동 기록 보기 상태로 바꾼다.
                            programState = ProgramState.PROGRAM_STATE_SHOW_RECORD
                            // 날짜별 운동 기록을 보는 상태의 세부 상태를 설정한다.
                            showRecordState = ShowRecordState.SHOW_RECORD_STATE_SELECT_DATE
                        }
                        // 종료 메뉴
                        MainMenuItem.MAIN_MENU_ITEM_EXIT.itemNumber -> {
                            // 종료 상태로 바꾼다.
                            programState = ProgramState.PROGRAM_STATE_EXIT
                        }
                    }
                }
                // 오늘의 운동을 기록하는 상태
                ProgramState.PROGRAM_STATE_WRITE_TODAY_RECORD -> {
                    // 오늘의 운동을 기록하는 메서드를 호출한다
                    inputRecordClass.inputTodayRecord()
                    // 메인 메뉴 상태로 바꾼다.
                    programState = ProgramState.PROGRAM_STATE_SHOW_MENU
                }
                // 날짜별 운동 기록을 보는 상태
                ProgramState.PROGRAM_STATE_SHOW_RECORD -> {

                    // 날짜별 운동 기록 상태의 세부 상태로 분기한다.
                    when(showRecordState){
                        // 기록한 날짜 목록을 보여주고 선택한다.
                        ShowRecordState.SHOW_RECORD_STATE_SELECT_DATE -> {
                            val inputNumber = showRecordClass.selectRecordDay()
                            if(inputNumber == 0){
                                programState = ProgramState.PROGRAM_STATE_SHOW_MENU
                            } else {
                                // 사용자 선택한 날짜의 번호를 변수에 담아준다.
                                showRecordClass.selectedRecordNumber = inputNumber
                                showRecordState = ShowRecordState.SHOW_RECORD_STATE_SHOW_RECORD
                            }
                        }
                        // 선택한 날짜의 운동 기록을 보여준다.
                        ShowRecordState.SHOW_RECORD_STATE_SHOW_RECORD -> {
                            // 선택한 날짜의 운동 기록을 보여준다.
                            showRecordClass.showSelectedRecord()
                            // 날짜 선택 상태로 바꾼다.
                            showRecordState = ShowRecordState.SHOW_RECORD_STATE_SELECT_DATE
                        }
                    }
                }
                // 종료
                ProgramState.PROGRAM_STATE_EXIT -> {
                    // 프로그램을 강제 종료시킨다
                    // 0 : 정상 종료를 나타내는 코드
                    println("프로그램을 종료합니다")
                    exitProcess(0)
                }
            }
        }
    }

    // 특정 날짜의 운동 기록을 가져오는 메서드
    fun getRecordData(fileName:String){

        // 리스트를 비운다.
        recordList.clear()

        // 파일에 기록된 데이터를 모두 불러온다.
        val fis = FileInputStream(fileName)
        val ois = ObjectInputStream(fis)

        try{
            while(true){
                val recordClass = ois.readObject() as MainClass.RecordClass
                recordList.add(recordClass)
            }
        }catch(e:Exception){
            ois.close()
            fis.close()
        }
    }
}

// 프로그램 상태를 나타내는 enum
enum class ProgramState{
    // 메인 메뉴를 보여주는 상태
    PROGRAM_STATE_SHOW_MENU,
    // 오늘의 운동을 기록하는 상태
    PROGRAM_STATE_WRITE_TODAY_RECORD,
    // 날짜별 운동 기록을 보는 상태
    PROGRAM_STATE_SHOW_RECORD,
    // 종료
    PROGRAM_STATE_EXIT
}
// InputRecordClass
import java.io.File
import java.io.FileOutputStream
import java.io.ObjectOutputStream
import java.util.*

class InputRecordClass(var scanner: Scanner, var mainClass: MainClass) {

    // 오늘의 운동을 기록하는 메서드
    fun inputTodayRecord() {
        while (true) {
            try {
                println()

                scanner.nextLine()

                print("운동 종류 : ")
                val type = scanner.nextLine()

                print("횟수 : ")
                val temp1 = scanner.next()
                val count = temp1.toInt()

                print("세트 : ")
                val temp2 = scanner.next()
                val set = temp2.toInt()

                // 오늘 날짜의 운동 기록을 가져온다.
                // 파일 이름을 가져온다.
                val todayRecordFileName = makeTodayFileName()

                mainClass.recordList.clear()

                val file = File(todayRecordFileName)
                if (file.exists() == true) {
                    // 오늘의 운동 기록 데이터를 읽어온다.
                    mainClass.getRecordData(todayRecordFileName)
                }

                // 새로운 데이터를 담고 있는 객체를 생성한다.
                val recordClass = MainClass.RecordClass(type, count, set)
                // 리스트에 담는다.
                mainClass.recordList.add(recordClass)

                // 현재 입력한 데이터를 저장한다.
                writeRecord(todayRecordFileName)

                break
            } catch (e: Exception) {
                println("잘못 입력하였습니다")
            }
        }
    }



    // 오늘 날짜를 통해 파일이름을 만들어준다.
    fun makeTodayFileName(): String {
        // 오늘 날짜를 가져온다.
        val calendar = Calendar.getInstance()
        val year = calendar.get(Calendar.YEAR)
        val month = calendar.get(Calendar.MONTH) + 1
        val date = calendar.get(Calendar.DAY_OF_MONTH)

        return "%d-%02d-%02d.record".format(year, month, date)
        // return "2023-05-25.record"
    }

    // 운동 기록 데이터를 저장한다.
    fun writeRecord(fileName: String) {

        val fos = FileOutputStream(fileName)
        val oos = ObjectOutputStream(fos)

        for (recordClass in mainClass.recordList) {
            oos.writeObject(recordClass)
        }

        oos.flush()
        oos.close()
        fos.close()
    }
}
// MainMenuClass
import java.util.*

class MainMenuClass(var scanner: Scanner){

    // 메인 메뉴를 보여주고 메뉴 번호를 입력받는다.
    fun inputMainMenuNumber() : Int{
        while(true) {
            try {
                println()
                println("메뉴를 선택해주세요")
                println("1. 오늘의 운동 기록")
                println("2. 날짜별 운동 기록 보기")
                println("3. 종료")
                print("메뉴 번호를 입력해주세요 : ")
                val inputNumberTemp = scanner.next()
                val inputNumber = inputNumberTemp.toInt()


                if (inputNumber !in 1..3) {
                    println("잘못 입력 하였습니다")
                } else {
                    return inputNumber
                }
            } catch(e:Exception){
                println("잘못 입력 하였습니다")
            }

        }
    }
}

// 메인 메뉴 항목
public enum class MainMenuItem(val itemNumber:Int){
    // 오늘의 운동 기록
    MAIN_MENU_ITEM_WRITE_TODAY_RECORD(1),
    // 날짜별 운동 기록 보기
    MAIN_MENU_ITEM_SHOW_RECORD(2),
    // 종료
    MAIN_MENU_ITEM_EXIT(3)
}
// ShowRecordClass

import java.io.File
import java.util.*

class ShowRecordClass(var scanner: Scanner, val mainClass: MainClass) {

    // 선택한 날짜 번호
    var selectedRecordNumber = 0

    // 기록을 볼 날짜를 선택하는 메서드
    fun selectRecordDay():Int{

        while(true) {
            try {
                println()
                // 등록된 날짜 목록을 출력한다.
                getRecordFileList()

                print("날짜를 선택해주세요(0. 이전) : ")

                val temp1 = scanner.next()
                val inputNumber = temp1.toInt()
                if(inputNumber !in 0..4){
                    println("잘못 입력하였습니다")
                } else {
                    return inputNumber
                }
            }catch(e:Exception){
                println("잘못 입력하였습니다.")
            }
        }
    }

    // 선택한 날짜의 운동 기록을 보여주는 메서드
    fun showSelectedRecord(){

        // 사용자가 선택한 번호의 파일 이름을 생성한다.
        val fileName = "${mainClass.recordFileList[selectedRecordNumber - 1]}.record"
        // 선택한 날짜의 운동 기록 데이터를 불러온다.
        mainClass.getRecordData(fileName)


        println()
        println("${mainClass.recordFileList[selectedRecordNumber - 1]}의 운동 기록입니다")

        for(recordClass in mainClass.recordList) {
            println()
            println("운동 타입 : ${recordClass.type}")
            println("횟수 : ${recordClass.count}")
            println("세트 : ${recordClass.set}")
        }
    }

    // 파일 목록을 불러온다.
    fun getRecordFileList(){
        // 파일 목록 리스트를 초기한다.
        mainClass.recordFileList.clear()

        // 현재 위치의 파일 목록을 가져온다.
        val dir = File(".")
        var fileList = dir.list()
        fileList = fileList.sortedArray()

        // 파일 목록에서 .record로 끝나는 것들만 담아 준다.
        for(temp1 in fileList){
            if(temp1.endsWith(".record")){
                val temp2 = temp1.replace(".record", "")
                mainClass.recordFileList.add(temp2)
            }
        }

        // 출력한다.
        if(mainClass.recordFileList.size == 0){
            println("등록된 운동 기록이 없습니다")
        } else {

            for (idx1 in 1..mainClass.recordFileList.size) {
                println("$idx1 : ${mainClass.recordFileList[idx1 - 1]}")
            }
        }
    }

}

enum class ShowRecordState{
    // 기록된 날짜를 선택하는 상태
    SHOW_RECORD_STATE_SELECT_DATE,
    // 선택된 날짜의 운동 기록을 보여주는 상태
    SHOW_RECORD_STATE_SHOW_RECORD
}

운동 기록 프로그램 설계 순서

  1. 예상 가능한 상태들을 enum class로 정의한다.
  2. 정의한 상태별로 while 문 내부를 분기한다.
  3. 각 상태에 대한 클래스들을 정의한다.
  4. 각 상태에서의 입출력 부분을 모두 구현한다.
  5. 구현된 화면을 보고 저장할 데이터들을 선별한다.
  6. 데이터 저장 관련 기능을 구현하면서 프로그램과 연동시킨다.

화면 입력 구현 후 생각해야할 것들...

화면 입력 구현이 모두 끝나면...
그 다음에는 화면을 구성하기 위해 필요한 데이터 중 변화는 것들, 사용자가 입력하는 것들, 그 외에 떠어르는 데이터들을 각 상태 별로 정리한다.
[메인 메뉴 화면]
입력받는 메뉴 번호
[오늘의 운동 기록]
운동 종류
횟수
세트
[날짜별 운동 기록 보기 - 기록 날짜 보는 상태]
날짜 선택 번호
기록된 날짜 목록
이전으로 돌아가는 번호
사용자가 입력하는 날짜 순번
입력할 날짜 값
[날짜별 운동 기록 보기 - 운동 기록 보는 상태]
선택한 날짜
운동타입
횟수
세트
그 다음에는 정리한 데이터들 중 저장해야 하는 것들을 선별한다.
선별된 것들을 클래스로 묶어 본다.
데이터 저장과 연동하여 나머지 기능들을 구현한다.

마무리

Enum 클래스를 효과적으로 활용하여 유지 보수성을 향상시키고, 각 상태에 대한 고려와 적절한 설계가 필요하다. 내가 작성한 코드와 강사님이 작성한 코드를 비교해보면, 구조를 체계적으로 설계하는 데 더 많은 노력이 필요하다. 클래스와 메서드가 많음에 불구하고 기능을 명확하게 파악할 수 있다. 또한, 내가 작성한 코드를 보면 중복되는 부분이 많다는 것을 깨달았고 다시 한번 프로그램을 작성해봐야 겠다.

profile
성장하는 개발자

0개의 댓글

관련 채용 정보