두 번째 미니 프로젝트로 운동 기록을 작성하고 날짜별로 작성된 기록을 출력하는 프로그램이다.
메뉴를 선택해주세요
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
}
- 예상 가능한 상태들을 enum class로 정의한다.
- 정의한 상태별로 while 문 내부를 분기한다.
- 각 상태에 대한 클래스들을 정의한다.
- 각 상태에서의 입출력 부분을 모두 구현한다.
- 구현된 화면을 보고 저장할 데이터들을 선별한다.
- 데이터 저장 관련 기능을 구현하면서 프로그램과 연동시킨다.
화면 입력 구현이 모두 끝나면...
그 다음에는 화면을 구성하기 위해 필요한 데이터 중 변화는 것들, 사용자가 입력하는 것들, 그 외에 떠어르는 데이터들을 각 상태 별로 정리한다.
[메인 메뉴 화면]
입력받는 메뉴 번호
[오늘의 운동 기록]
운동 종류
횟수
세트
[날짜별 운동 기록 보기 - 기록 날짜 보는 상태]
날짜 선택 번호
기록된 날짜 목록
이전으로 돌아가는 번호
사용자가 입력하는 날짜 순번
입력할 날짜 값
[날짜별 운동 기록 보기 - 운동 기록 보는 상태]
선택한 날짜
운동타입
횟수
세트
그 다음에는 정리한 데이터들 중 저장해야 하는 것들을 선별한다.
선별된 것들을 클래스로 묶어 본다.
데이터 저장과 연동하여 나머지 기능들을 구현한다.
Enum 클래스를 효과적으로 활용하여 유지 보수성을 향상시키고, 각 상태에 대한 고려와 적절한 설계가 필요하다. 내가 작성한 코드와 강사님이 작성한 코드를 비교해보면, 구조를 체계적으로 설계하는 데 더 많은 노력이 필요하다. 클래스와 메서드가 많음에 불구하고 기능을 명확하게 파악할 수 있다. 또한, 내가 작성한 코드를 보면 중복되는 부분이 많다는 것을 깨달았고 다시 한번 프로그램을 작성해봐야 겠다.